Skip to content

Commit 8973a4d

Browse files
committed
feat(backend): add Completion Condition mechanism for CI monitoring
Add a generic Completion Condition system to track async external conditions (e.g., CI pipelines) and trigger auto-fix workflows: - Add CompletionCondition model with status tracking and retry logic - Add GitHub/GitLab webhook endpoints for receiving CI events - Add CIMonitorService for handling CI events and auto-fix workflow - Add CompletionConditionService for CRUD operations - Add new webhook notification events: condition.satisfied, condition.ci_failed, condition.max_retry_reached, task.fully_completed - Add configuration for CI monitoring (CI_MONITOR_ENABLED, CI_MAX_RETRIES) - Add database migration for completion_conditions table The system automatically detects CI failures and creates fix subtasks with relevant logs, up to a configurable max retry limit.
1 parent b073446 commit 8973a4d

File tree

13 files changed

+1578
-2
lines changed

13 files changed

+1578
-2
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""add completion_conditions table for CI monitoring
2+
3+
Revision ID: 2b3c4d5e6f7g
4+
Revises: 1a2b3c4d5e6f
5+
Create Date: 2025-07-01 12:00:00.000000+08:00
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '2b3c4d5e6f7g'
16+
down_revision: Union[str, Sequence[str], None] = '1a2b3c4d5e6f'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Add completion_conditions table for tracking async completion conditions."""
23+
24+
# Create completion_conditions table
25+
op.execute("""
26+
CREATE TABLE IF NOT EXISTS completion_conditions (
27+
id INT NOT NULL AUTO_INCREMENT,
28+
subtask_id INT NOT NULL,
29+
task_id INT NOT NULL,
30+
user_id INT NOT NULL,
31+
condition_type ENUM('CI_PIPELINE', 'EXTERNAL_TASK', 'APPROVAL', 'MANUAL_CONFIRM') NOT NULL DEFAULT 'CI_PIPELINE',
32+
status ENUM('PENDING', 'IN_PROGRESS', 'SATISFIED', 'FAILED', 'CANCELLED') NOT NULL DEFAULT 'PENDING',
33+
external_id VARCHAR(256) DEFAULT NULL,
34+
external_url VARCHAR(1024) DEFAULT NULL,
35+
git_platform ENUM('GITHUB', 'GITLAB') DEFAULT NULL,
36+
git_domain VARCHAR(256) DEFAULT NULL,
37+
repo_full_name VARCHAR(512) DEFAULT NULL,
38+
branch_name VARCHAR(256) DEFAULT NULL,
39+
retry_count INT NOT NULL DEFAULT 0,
40+
max_retries INT NOT NULL DEFAULT 5,
41+
last_failure_log TEXT DEFAULT NULL,
42+
metadata JSON DEFAULT NULL,
43+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
44+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
45+
satisfied_at DATETIME DEFAULT NULL,
46+
PRIMARY KEY (id),
47+
KEY ix_completion_conditions_id (id),
48+
KEY ix_completion_conditions_subtask_id (subtask_id),
49+
KEY ix_completion_conditions_task_id (task_id),
50+
KEY ix_completion_conditions_user_id (user_id),
51+
KEY ix_completion_conditions_branch_name (branch_name),
52+
KEY ix_completion_conditions_repo_branch (repo_full_name, branch_name)
53+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
54+
""")
55+
56+
57+
def downgrade() -> None:
58+
"""Remove completion_conditions table."""
59+
op.execute("DROP TABLE IF EXISTS completion_conditions")

backend/app/api/api.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
from app.api.endpoints import admin, auth, oidc, quota, repository, users
5+
from app.api.endpoints import admin, auth, completion_conditions, oidc, quota, repository, users
66
from app.api.endpoints.adapter import (
77
agents,
88
bots,
@@ -13,6 +13,7 @@
1313
teams,
1414
)
1515
from app.api.endpoints.kind import k_router
16+
from app.api.endpoints.webhooks import github_router, gitlab_router
1617
from app.api.router import api_router
1718

1819
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
@@ -29,3 +30,18 @@
2930
api_router.include_router(quota.router, prefix="/quota", tags=["quota"])
3031
api_router.include_router(dify.router, prefix="/dify", tags=["dify"])
3132
api_router.include_router(k_router)
33+
34+
# Completion conditions and CI monitoring
35+
api_router.include_router(
36+
completion_conditions.router,
37+
prefix="/completion-conditions",
38+
tags=["completion-conditions"],
39+
)
40+
41+
# External webhooks (no auth required)
42+
api_router.include_router(
43+
github_router, prefix="/webhooks/github", tags=["webhooks"]
44+
)
45+
api_router.include_router(
46+
gitlab_router, prefix="/webhooks/gitlab", tags=["webhooks"]
47+
)

backend/app/api/dependencies.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from contextlib import contextmanager
56
from typing import Generator
67

78
from sqlalchemy.orm import Session
@@ -19,3 +20,16 @@ def get_db() -> Generator[Session, None, None]:
1920
yield db
2021
finally:
2122
db.close()
23+
24+
25+
@contextmanager
26+
def get_db_context() -> Generator[Session, None, None]:
27+
"""
28+
Database session context manager for use outside of FastAPI dependency injection.
29+
Use this when you need a database session in async functions or background tasks.
30+
"""
31+
db = SessionLocal()
32+
try:
33+
yield db
34+
finally:
35+
db.close()
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""
6+
Completion Conditions API endpoints
7+
"""
8+
from typing import List, Optional
9+
10+
from fastapi import APIRouter, Depends, HTTPException, Query
11+
from sqlalchemy.orm import Session
12+
13+
from app.api.dependencies import get_db
14+
from app.core import security
15+
from app.models.user import User
16+
from app.schemas.completion_condition import (
17+
CompletionConditionCreate,
18+
CompletionConditionInDB,
19+
CompletionConditionListResponse,
20+
TaskCompletionStatus,
21+
)
22+
from app.services.completion_condition import completion_condition_service
23+
24+
router = APIRouter()
25+
26+
27+
@router.get("", response_model=CompletionConditionListResponse)
28+
def list_completion_conditions(
29+
subtask_id: Optional[int] = Query(None, description="Filter by subtask ID"),
30+
task_id: Optional[int] = Query(None, description="Filter by task ID"),
31+
db: Session = Depends(get_db),
32+
current_user: User = Depends(security.get_current_user),
33+
):
34+
"""
35+
List completion conditions with optional filters.
36+
At least one of subtask_id or task_id must be provided.
37+
"""
38+
if subtask_id is None and task_id is None:
39+
raise HTTPException(
40+
status_code=400,
41+
detail="At least one of subtask_id or task_id must be provided",
42+
)
43+
44+
if subtask_id:
45+
conditions = completion_condition_service.get_by_subtask_id(
46+
db, subtask_id=subtask_id, user_id=current_user.id
47+
)
48+
else:
49+
conditions = completion_condition_service.get_by_task_id(
50+
db, task_id=task_id, user_id=current_user.id
51+
)
52+
53+
return CompletionConditionListResponse(total=len(conditions), items=conditions)
54+
55+
56+
@router.get("/{condition_id}", response_model=CompletionConditionInDB)
57+
def get_completion_condition(
58+
condition_id: int,
59+
db: Session = Depends(get_db),
60+
current_user: User = Depends(security.get_current_user),
61+
):
62+
"""Get a specific completion condition by ID"""
63+
condition = completion_condition_service.get_by_id(
64+
db, condition_id=condition_id, user_id=current_user.id
65+
)
66+
if not condition:
67+
raise HTTPException(status_code=404, detail="Completion condition not found")
68+
return condition
69+
70+
71+
@router.post("", response_model=CompletionConditionInDB)
72+
def create_completion_condition(
73+
condition_in: CompletionConditionCreate,
74+
db: Session = Depends(get_db),
75+
current_user: User = Depends(security.get_current_user),
76+
):
77+
"""Create a new completion condition"""
78+
condition = completion_condition_service.create_condition(
79+
db, obj_in=condition_in, user_id=current_user.id
80+
)
81+
return condition
82+
83+
84+
@router.delete("/{condition_id}/cancel")
85+
def cancel_completion_condition(
86+
condition_id: int,
87+
db: Session = Depends(get_db),
88+
current_user: User = Depends(security.get_current_user),
89+
):
90+
"""Cancel a completion condition"""
91+
from app.models.completion_condition import ConditionStatus
92+
93+
condition = completion_condition_service.get_by_id(
94+
db, condition_id=condition_id, user_id=current_user.id
95+
)
96+
if not condition:
97+
raise HTTPException(status_code=404, detail="Completion condition not found")
98+
99+
if condition.status in [ConditionStatus.SATISFIED, ConditionStatus.FAILED]:
100+
raise HTTPException(
101+
status_code=400,
102+
detail=f"Cannot cancel condition in {condition.status} status",
103+
)
104+
105+
condition = completion_condition_service.update_status(
106+
db, condition_id=condition_id, status=ConditionStatus.CANCELLED
107+
)
108+
return {"status": "cancelled", "id": condition_id}
109+
110+
111+
@router.get("/tasks/{task_id}/completion-status", response_model=TaskCompletionStatus)
112+
def get_task_completion_status(
113+
task_id: int,
114+
db: Session = Depends(get_db),
115+
current_user: User = Depends(security.get_current_user),
116+
):
117+
"""
118+
Get the overall completion status for a task,
119+
including all completion conditions and their status.
120+
"""
121+
status = completion_condition_service.get_task_completion_status(
122+
db, task_id=task_id, user_id=current_user.id
123+
)
124+
125+
# Convert conditions to schema objects
126+
from app.schemas.completion_condition import CompletionConditionInDB
127+
128+
conditions_in_db = [
129+
CompletionConditionInDB.model_validate(c) for c in status["conditions"]
130+
]
131+
132+
return TaskCompletionStatus(
133+
task_id=task_id,
134+
subtask_completed=True, # This would need to be checked from subtask status
135+
all_conditions_satisfied=status["all_conditions_satisfied"],
136+
pending_conditions=status["pending_conditions"],
137+
in_progress_conditions=status["in_progress_conditions"],
138+
satisfied_conditions=status["satisfied_conditions"],
139+
failed_conditions=status["failed_conditions"],
140+
conditions=conditions_in_db,
141+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""
6+
Webhooks package for handling external CI events
7+
"""
8+
from app.api.endpoints.webhooks.github import router as github_router
9+
from app.api.endpoints.webhooks.gitlab import router as gitlab_router
10+
11+
__all__ = ["github_router", "gitlab_router"]

0 commit comments

Comments
 (0)