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
50 changes: 50 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,56 @@ configs/kimi-local-integration.dashscope.env.example

它可以从 Web UI 或 `PUT /api/settings/runtime` 写入,随后会自动注入 Worker 的 `GITHUB_TOKEN` / `GH_TOKEN`。

同一个运行时设置里还包含:

- `auto_review_merge_reload`

开启后,worker 完成并返回 `pr_url` 时,系统会优先执行自动路径:

1. 用 GitHub API 给 PR approve
2. 用 GitHub API 做 squash merge
3. 删除 worker 分支
4. 将 incident 标记为 `approved`
5. 由 deploy watcher 拉取目标分支并触发热重载

如果自动路径失败,系统会回退到飞书 review 通知流程。

服务器侧建议补充配置:

- `CODE_TERMINATOR_DEPLOY_BRANCH`:deploy watcher 要拉取的目标分支
- `CODE_TERMINATOR_AGENT_ENABLE_INGEST=1`:保持日志监听开启

GitHub token 需要具备:

- PR review 权限
- PR merge 权限
- 删除分支权限
- 若仓库启用了分支保护,仍需满足对应保护规则

同一组运行时设置里还包含:

- `auto_review_merge_reload`

这个开关开启后,worker 完成并返回 `pr_url` 时,系统会优先走自动路径:

1. 用 GitHub API 给 PR 执行 approve
2. 用 GitHub API 执行 squash merge
3. 删除 worker 创建的分支
4. 将 incident 标记为 `approved`
5. 由 deploy watcher 拉取最新代码并触发热重载

如果自动路径失败,系统会回退到原来的飞书 review 通知流程。

服务器侧建议额外配置:

- `CODE_TERMINATOR_DEPLOY_BRANCH`:热重载时要拉取的目标分支,默认优先使用当前检出分支,未能识别时回退到 `main`
- `CODE_TERMINATOR_AGENT_ENABLE_INGEST=1`:保持日志监听启用

注意:

- GitHub token 需要具备 PR review / merge / delete branch 权限
- 如果仓库启用了强制人工 review 或分支保护,自动 merge 仍可能被 GitHub 拒绝,这是仓库规则,不是本地代码问题

### Leader Tuning

| 变量 | 默认值 | 说明 |
Expand Down
36 changes: 36 additions & 0 deletions docs/auto-review-server-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Auto Review / Merge / Reload Server Checklist

Use this checklist before enabling automatic incident repair on a server clone.

## Required Runtime Settings

- Save a GitHub token from the Web UI or `PUT /api/settings/runtime`.
- Enable `auto_review_merge_reload` in the same runtime settings payload.
- Set `CODE_TERMINATOR_AGENT_ENABLE_INGEST=1` so the log listener stays active.
- Set `CODE_TERMINATOR_DEPLOY_BRANCH` to the branch the server should pull after merge.

Example:

```bash
export CODE_TERMINATOR_AGENT_ENABLE_INGEST=1
export CODE_TERMINATOR_DEPLOY_BRANCH=main
```

## GitHub Token Permissions

The token must be able to:

- review pull requests
- merge pull requests
- delete worker branches after merge
- satisfy any branch protection rules configured on the target branch

## Expected Flow

1. The log listener opens or resumes an incident.
2. The worker fixes the issue and returns `workflow_updates.pr_url`.
3. The runtime auto-approves and squash-merges the PR through the GitHub API.
4. The incident is marked `approved`.
5. The deploy watcher pulls `CODE_TERMINATOR_DEPLOY_BRANCH`.
6. The ecommerce reload stack refreshes and health checks the gateway.
7. The incident moves from `deployed` to `resolved` after the verification window.
2 changes: 2 additions & 0 deletions src/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ class ChatSendRequest(BaseModel):

class RuntimeSettingsResponse(BaseModel):
github_token: str = ""
auto_review_merge_reload: bool = False
updated_at: str = Field(default_factory=now_iso)


class RuntimeSettingsUpdateRequest(BaseModel):
github_token: str = ""
auto_review_merge_reload: bool = False


class PlanItemPayload(BaseModel):
Expand Down
48 changes: 36 additions & 12 deletions src/api/services/runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import os
import shutil
from urllib.parse import quote
import uuid
from dataclasses import dataclass, field
from pathlib import Path
Expand Down Expand Up @@ -81,7 +82,10 @@ def get_runtime_settings(self) -> RuntimeSettingsResponse:
def update_runtime_settings(
self, request: RuntimeSettingsUpdateRequest
) -> RuntimeSettingsResponse:
settings = save_runtime_settings(github_token=request.github_token)
settings = save_runtime_settings(
github_token=request.github_token,
auto_review_merge_reload=request.auto_review_merge_reload,
)
return RuntimeSettingsResponse.model_validate(settings.model_dump())

def list_agent_status(self) -> AgentStatusResponse:
Expand Down Expand Up @@ -641,20 +645,35 @@ async def _dispatch_hook_event(
workflow_updates = details.get("workflow_updates", {})
if thread_id.startswith("incident::"):
fingerprint = thread_id.replace("incident::", "")
from src.app.auto_review_merge import (
auto_review_merge_reload,
)
from src.app.incident_registry import get as get_incident
from src.app.review_bridge import send_review_notification

entry = get_incident(fingerprint)
if entry and entry.get("status") not in ("resolved", "suppressed"):
send_review_notification(
fingerprint=fingerprint,
service=entry.get("service", ""),
exception_type=entry.get("exception_type", ""),
traceback_summary=entry.get("sample_traceback", ""),
branch_name=str(workflow_updates.get("branch_name", "")),
commit_sha=str(workflow_updates.get("commit_sha", "")),
pr_url=str(workflow_updates.get("pr_url", "")),
)
settings = load_runtime_settings()
common = {
"fingerprint": fingerprint,
"service": entry.get("service", ""),
"exception_type": entry.get("exception_type", ""),
"traceback_summary": entry.get("sample_traceback", ""),
"branch_name": str(workflow_updates.get("branch_name", "")),
"commit_sha": str(workflow_updates.get("commit_sha", "")),
"pr_url": str(workflow_updates.get("pr_url", "")),
}
if settings.auto_review_merge_reload:
result = auto_review_merge_reload(**common)
if not result.get("ok"):
logger.warning(
"auto_review_merge.failed fingerprint=%s error=%s",
fingerprint,
result.get("error", ""),
)
send_review_notification(**common)
else:
send_review_notification(**common)
except Exception as exc:
logger.warning("review_bridge.auto_notify.error error=%s", exc)
return True
Expand Down Expand Up @@ -769,10 +788,10 @@ def _reset_startup_runtime_state(self) -> None:
}

def _conversation_path(self, conversation_id: str) -> Path:
return self._state_root / "conversations" / f"{conversation_id}.json"
return self._state_root / "conversations" / f"{_safe_runtime_filename(conversation_id)}.json"

def _plan_path(self, conversation_id: str) -> Path:
return self._state_root / "plans" / f"{conversation_id}.json"
return self._state_root / "plans" / f"{_safe_runtime_filename(conversation_id)}.json"

def _conversation_id_for_thread(self, thread_id: str) -> str:
for conversation_id, stored_thread_id in self._threads.items():
Expand Down Expand Up @@ -845,3 +864,8 @@ def _load_plan_snapshot_from_disk(
return PlanSnapshotResponse.model_validate(payload)
except Exception:
return None


def _safe_runtime_filename(value: str) -> str:
encoded = quote(value.strip() or "default", safe="")
return encoded or "default"
Loading
Loading