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
3 changes: 3 additions & 0 deletions .mergequeue.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ branch_prefix = "deploybot/integration"
title_prefix = "DeployBot integration"
max_batch_size = 3
require_non_actions_author = false
# Name only pull_request-only wrapper checks that the successful exact-head
# pipeline.ci_workflows run already proves. Independent checks stay mandatory.
ci_satisfies_checks = []
7 changes: 4 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.16` source commit directly from GitHub:
Install the reviewed `v0.2.17` source commit directly from GitHub:

```bash
python3 -m pip install \
'deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@415f0a360bad2e54b156b330c2d21e3a64823a5a'
'deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@7842edbd5fe75cdb68324e98c2a7244aed3009f6'
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@415f0a360bad2e54b156b330c2d21e3a64823a5a
- uses: Forward-Future/DeployBot@7842edbd5fe75cdb68324e98c2a7244aed3009f6
```

The Action uses GitHub's built-in workflow token. GitHub intentionally does not
Expand Down Expand Up @@ -264,6 +264,7 @@ expected_status = 200
mode = "overlap"
max_batch_size = 3
# require_non_actions_author = true
# ci_satisfies_checks = ["Stable PR head", "Full test suite"]
```

For `overlap` or `all` mode with the hosted coordinator, enable **Allow GitHub
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.16",
"version": "0.2.17",
"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@415f0a360bad2e54b156b330c2d21e3a64823a5a",
"deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@7842edbd5fe75cdb68324e98c2a7244aed3009f6",
"deploybot-mcp"
]
}
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.16",
"version": "0.2.17",
"description": "Coordinate exact-head pull requests through verified deployment and thread notification",
"author": {
"name": "DeployBot contributors"
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@415f0a360bad2e54b156b330c2d21e3a64823a5a",
"deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@7842edbd5fe75cdb68324e98c2a7244aed3009f6",
"deploybot-mcp"
]
}
Expand Down
3 changes: 2 additions & 1 deletion 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.16. GitHub labels and authenticated comments are the durable state;
DeployBot v0.2.17. GitHub labels and authenticated comments are the durable state;
the CLI and MCP tools are two interfaces to the same operations.

## CLI
Expand Down Expand Up @@ -191,6 +191,7 @@ Provider fields are:
| `title_prefix` | `"DeployBot integration"` |
| `max_batch_size` | Positive maximum frozen batch size; default 3. Later FIFO entries remain in the next batch. A larger indivisible source-overlap or dependency closure ships alone rather than being split or deadlocked. |
| `require_non_actions_author` | Default `false`; when `true`, integration creation requires the Action `token` input and an App bot author listed in `queue.coordinator_actors`. |
| `ci_satisfies_checks` | Default `[]`; explicit pull-request-only wrapper checks already proved by successful exact-head `pipeline.ci_workflows`. Must be a subset of `queue.required_checks` and never overrides a failed check. |

## GitHub Action

Expand Down
4 changes: 2 additions & 2 deletions examples/github-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ jobs:
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
# v0.2.16 implementation; keep the full commit for privileged workflows.
- uses: Forward-Future/DeployBot@415f0a360bad2e54b156b330c2d21e3a64823a5a
# v0.2.17 implementation; keep the full commit for privileged workflows.
- uses: Forward-Future/DeployBot@7842edbd5fe75cdb68324e98c2a7244aed3009f6
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.16"
version = "0.2.17"
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.16"
__version__ = "0.2.17"
85 changes: 79 additions & 6 deletions src/agent_merge_queue/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ def check_states(checks: Iterable[dict[str, Any]]) -> dict[str, str]:
return result


def merge_known_check_states(
observed: dict[str, str],
known: dict[str, str] | None,
) -> dict[str, str]:
result = dict(observed)
for name, state in (known or {}).items():
if result.get(name) == "failed" and state != "failed":
continue
result[name] = state
return result


def latest_exact_workflow_runs(
runs: Iterable[dict[str, Any]],
names: Iterable[str],
Expand Down Expand Up @@ -1999,8 +2011,7 @@ def snapshot(
marker = queue_marker_for_client(self, comments)
head_sha = str(pull["headRefOid"])
check_rollup = list(pull.get("statusCheckRollup") or [])
checks = check_states(check_rollup)
checks.update(known_checks or {})
checks = merge_known_check_states(check_states(check_rollup), known_checks)
integration = latest_payload(
comments,
INTEGRATION_MARKER,
Expand All @@ -2009,8 +2020,10 @@ def snapshot(
if integration and any(
checks.get(name) != "passed" for name in self.config.required_checks
):
checks = check_states(check_rollup + self.commit_check_runs(head_sha))
checks.update(known_checks or {})
checks = merge_known_check_states(
check_states(check_rollup + self.commit_check_runs(head_sha)),
known_checks,
)
source_paths = list(known_source_paths or [])
generated_paths = list(known_generated_paths or [])
paths_are_known = (
Expand Down Expand Up @@ -3414,13 +3427,18 @@ def settle_integration_checks(
and str(run.get("conclusion") or "") == "success"
for run in latest.values()
):
exact_checks = check_states(client.commit_check_runs(head_sha))
exact_checks = integration_owned_check_states(client, head_sha)
entry = client.snapshot(
number,
require_marker=False,
allow_blocked_label=True,
known_checks=exact_checks,
)
if entry.head_sha != head_sha:
raise QueueError(
f"integration PR #{number} changed while CI evidence "
"was being applied"
)
if entry.state == "ready":
results.append(
{
Expand Down Expand Up @@ -3449,6 +3467,44 @@ def settle_integration_checks(
return results


def integration_owned_check_states(client: GitHub, head_sha: str) -> dict[str, str]:
"""Use successful exact integration CI as the required-check aggregate."""

checks = check_states(client.commit_check_runs(head_sha))
# Token-authored integration PRs do not emit every pull_request-only wrapper
# check. The configured workflow's exact-head success is the aggregate proof
# for those required checks; review providers remain separate and unchanged.
for name in client.config.integration.ci_satisfies_checks:
if checks.get(name) != "failed":
checks[name] = "passed"
return checks


def completed_integration_ci_checks(
client: GitHub,
*,
branch: str,
head_sha: str,
) -> dict[str, str] | None:
configured = tuple(client.config.pipeline.ci_workflows)
if not configured:
return None
latest = latest_exact_workflow_runs(
client.workflow_runs_for_branch(branch),
configured,
head_sha=head_sha,
)
if any(name not in latest for name in configured):
return None
if not all(
str(run.get("status") or "") == "completed"
and str(run.get("conclusion") or "") == "success"
for run in latest.values()
):
return None
return integration_owned_check_states(client, head_sha)


def command_resume(client: GitHub, selector: str | None) -> None:
number = client.resolve_pr(selector)
comments = client.comments(number)
Expand All @@ -3458,7 +3514,24 @@ def command_resume(client: GitHub, selector: str | None) -> None:
coordinator_logins(client),
)
if integration:
entry = client.snapshot(number, require_marker=False, allow_blocked_label=True)
pull = client.pull_head(number)
branch = str(pull.get("branch") or "")
head_sha = str(pull.get("head_sha") or "")
known_checks = completed_integration_ci_checks(
client,
branch=branch,
head_sha=head_sha,
)
entry = client.snapshot(
number,
require_marker=False,
allow_blocked_label=True,
known_checks=known_checks,
)
if entry.head_sha != head_sha:
raise QueueError(
f"integration PR #{number} changed while CI evidence was being applied"
)
for source_number, source_head in (integration.get("heads") or {}).items():
if not client.is_ancestor(str(source_head), entry.head_sha):
raise QueueError(
Expand Down
28 changes: 24 additions & 4 deletions src/agent_merge_queue/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class IntegrationConfig:
title_prefix: str
max_batch_size: int
require_non_actions_author: bool
ci_satisfies_checks: tuple[str, ...]


@dataclass(frozen=True)
Expand Down Expand Up @@ -146,6 +147,8 @@ class QueueConfig:
title_prefix = "DeployBot integration"
max_batch_size = 3
require_non_actions_author = false
# Exact integration CI may replace only these pull_request-only wrapper checks.
ci_satisfies_checks = []
"""


Expand Down Expand Up @@ -328,6 +331,10 @@ def parse_config(payload: dict[str, Any]) -> QueueConfig:
raise ConfigError("review must be a table")
if not isinstance(pipeline, dict) or not isinstance(integration, dict):
raise ConfigError("pipeline and integration must be tables")
ci_workflows = _string_tuple(
pipeline.get("ci_workflows", ["CI"]),
"pipeline.ci_workflows",
)

merge_method = _require_string(
queue.get("merge_method"), "queue.merge_method", "merge"
Expand Down Expand Up @@ -386,6 +393,21 @@ def parse_config(payload: dict[str, Any]) -> QueueConfig:
if integration_mode not in ALLOWED_INTEGRATION_MODES:
allowed = ", ".join(sorted(ALLOWED_INTEGRATION_MODES))
raise ConfigError(f"integration.mode must be one of: {allowed}")
ci_satisfies_checks = _string_tuple(
integration.get("ci_satisfies_checks"),
"integration.ci_satisfies_checks",
)
unknown_ci_checks = sorted(set(ci_satisfies_checks) - set(required_checks))
if unknown_ci_checks:
raise ConfigError(
"integration.ci_satisfies_checks must be a subset of "
"queue.required_checks: " + ", ".join(unknown_ci_checks)
)
if ci_satisfies_checks and not ci_workflows:
raise ConfigError(
"integration.ci_satisfies_checks requires at least one "
"pipeline.ci_workflows entry"
)
webhook_url_env = pipeline.get("webhook_url_env")
if webhook_url_env is not None:
webhook_url_env = _require_string(webhook_url_env, "pipeline.webhook_url_env")
Expand Down Expand Up @@ -454,10 +476,7 @@ def parse_config(payload: dict[str, Any]) -> QueueConfig:
"pipeline.thread_active_hours",
72,
),
ci_workflows=_string_tuple(
pipeline.get("ci_workflows", ["CI"]),
"pipeline.ci_workflows",
),
ci_workflows=ci_workflows,
deploy_workflows=_string_tuple(
pipeline.get("deploy_workflows", ["Deploy"]),
"pipeline.deploy_workflows",
Expand Down Expand Up @@ -536,6 +555,7 @@ def parse_config(payload: dict[str, Any]) -> QueueConfig:
"integration.require_non_actions_author",
False,
),
ci_satisfies_checks=ci_satisfies_checks,
),
)

Expand Down
Loading