Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""add completion_conditions table for CI monitoring

Revision ID: 2b3c4d5e6f7g
Revises: 1a2b3c4d5e6f
Create Date: 2025-07-01 12:00:00.000000+08:00

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '2b3c4d5e6f7g'
down_revision: Union[str, Sequence[str], None] = '1a2b3c4d5e6f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Add completion_conditions table for tracking async completion conditions."""

# Create completion_conditions table
op.execute("""
CREATE TABLE IF NOT EXISTS completion_conditions (
id INT NOT NULL AUTO_INCREMENT,
subtask_id INT NOT NULL,
task_id INT NOT NULL,
user_id INT NOT NULL,
condition_type ENUM('CI_PIPELINE', 'EXTERNAL_TASK', 'APPROVAL', 'MANUAL_CONFIRM') NOT NULL DEFAULT 'CI_PIPELINE',
status ENUM('PENDING', 'IN_PROGRESS', 'SATISFIED', 'FAILED', 'CANCELLED') NOT NULL DEFAULT 'PENDING',
external_id VARCHAR(256) DEFAULT NULL,
external_url VARCHAR(1024) DEFAULT NULL,
git_platform ENUM('GITHUB', 'GITLAB') DEFAULT NULL,
git_domain VARCHAR(256) DEFAULT NULL,
repo_full_name VARCHAR(512) DEFAULT NULL,
branch_name VARCHAR(256) DEFAULT NULL,
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 5,
last_failure_log TEXT DEFAULT NULL,
metadata JSON DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
satisfied_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY ix_completion_conditions_id (id),
KEY ix_completion_conditions_subtask_id (subtask_id),
KEY ix_completion_conditions_task_id (task_id),
KEY ix_completion_conditions_user_id (user_id),
KEY ix_completion_conditions_branch_name (branch_name),
KEY ix_completion_conditions_repo_branch (repo_full_name, branch_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
""")


def downgrade() -> None:
"""Remove completion_conditions table."""
op.execute("DROP TABLE IF EXISTS completion_conditions")
18 changes: 17 additions & 1 deletion backend/app/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0

from app.api.endpoints import admin, auth, oidc, quota, repository, users
from app.api.endpoints import admin, auth, completion_conditions, oidc, quota, repository, users
from app.api.endpoints.adapter import (
agents,
bots,
Expand All @@ -13,6 +13,7 @@
teams,
)
from app.api.endpoints.kind import k_router
from app.api.endpoints.webhooks import github_router, gitlab_router
from app.api.router import api_router

api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
Expand All @@ -29,3 +30,18 @@
api_router.include_router(quota.router, prefix="/quota", tags=["quota"])
api_router.include_router(dify.router, prefix="/dify", tags=["dify"])
api_router.include_router(k_router)

# Completion conditions and CI monitoring
api_router.include_router(
completion_conditions.router,
prefix="/completion-conditions",
tags=["completion-conditions"],
)

# External webhooks (no auth required)
api_router.include_router(
github_router, prefix="/webhooks/github", tags=["webhooks"]
)
api_router.include_router(
gitlab_router, prefix="/webhooks/gitlab", tags=["webhooks"]
)
14 changes: 14 additions & 0 deletions backend/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0

from contextlib import contextmanager
from typing import Generator

from sqlalchemy.orm import Session
Expand All @@ -19,3 +20,16 @@ def get_db() -> Generator[Session, None, None]:
yield db
finally:
db.close()


@contextmanager
def get_db_context() -> Generator[Session, None, None]:
"""
Database session context manager for use outside of FastAPI dependency injection.
Use this when you need a database session in async functions or background tasks.
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
141 changes: 141 additions & 0 deletions backend/app/api/endpoints/completion_conditions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""
Completion Conditions API endpoints
"""
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session

from app.api.dependencies import get_db
from app.core import security
from app.models.user import User
from app.schemas.completion_condition import (
CompletionConditionCreate,
CompletionConditionInDB,
CompletionConditionListResponse,
TaskCompletionStatus,
)
from app.services.completion_condition import completion_condition_service

router = APIRouter()


@router.get("", response_model=CompletionConditionListResponse)
def list_completion_conditions(
subtask_id: Optional[int] = Query(None, description="Filter by subtask ID"),
task_id: Optional[int] = Query(None, description="Filter by task ID"),
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""
List completion conditions with optional filters.
At least one of subtask_id or task_id must be provided.
"""
if subtask_id is None and task_id is None:
raise HTTPException(
status_code=400,
detail="At least one of subtask_id or task_id must be provided",
)

if subtask_id:
conditions = completion_condition_service.get_by_subtask_id(
db, subtask_id=subtask_id, user_id=current_user.id
)
else:
conditions = completion_condition_service.get_by_task_id(
db, task_id=task_id, user_id=current_user.id
)

return CompletionConditionListResponse(total=len(conditions), items=conditions)


@router.get("/{condition_id}", response_model=CompletionConditionInDB)
def get_completion_condition(
condition_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Get a specific completion condition by ID"""
condition = completion_condition_service.get_by_id(
db, condition_id=condition_id, user_id=current_user.id
)
if not condition:
raise HTTPException(status_code=404, detail="Completion condition not found")
return condition
Comment on lines +56 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Route path collision—/tasks/{task_id}/... will be captured by /{condition_id}.

FastAPI matches routes in declaration order. The /{condition_id} route (line 56) will match requests intended for /tasks/{task_id}/completion-status (line 111), treating "tasks" as the condition_id. This will result in a validation error or 404.

Option 1 (preferred): Reorder routes so the more specific path comes first:

 router = APIRouter()
 
+@router.get("/tasks/{task_id}/completion-status", response_model=TaskCompletionStatus)
+def get_task_completion_status(
+    task_id: int,
+    db: Session = Depends(get_db),
+    current_user: User = Depends(security.get_current_user),
+):
+    # ... implementation
+
 @router.get("", response_model=CompletionConditionListResponse)
 def list_completion_conditions(...):
     ...

 @router.get("/{condition_id}", response_model=CompletionConditionInDB)
 def get_completion_condition(...):
     ...
-
-@router.get("/tasks/{task_id}/completion-status", response_model=TaskCompletionStatus)
-def get_task_completion_status(...):
-    ...

Option 2: Change the path to avoid collision, e.g., use a query parameter or a different path structure like /task-status/{task_id}.

Also applies to: 111-140

🧰 Tools
🪛 Ruff (0.14.6)

59-59: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


60-60: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In backend/app/api/endpoints/completion_conditions.py around lines 56-68 (and
related handlers at 111-140), the GET route path "/{condition_id}" will greedily
match requests like "/tasks/{task_id}/completion-status", causing route
collision; fix by reordering routes so the more specific
"/tasks/{task_id}/completion-status" (lines 111-140) is declared before the
generic "/{condition_id}" route, or alternatively change the generic route to a
non-conflicting path (e.g., "/by-id/{condition_id}" or
"/condition/{condition_id}") and update usages/tests accordingly.



@router.post("", response_model=CompletionConditionInDB)
def create_completion_condition(
condition_in: CompletionConditionCreate,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Create a new completion condition"""
condition = completion_condition_service.create_condition(
db, obj_in=condition_in, user_id=current_user.id
)
return condition


@router.delete("/{condition_id}/cancel")
def cancel_completion_condition(
condition_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Cancel a completion condition"""
from app.models.completion_condition import ConditionStatus

condition = completion_condition_service.get_by_id(
db, condition_id=condition_id, user_id=current_user.id
)
if not condition:
raise HTTPException(status_code=404, detail="Completion condition not found")

if condition.status in [ConditionStatus.SATISFIED, ConditionStatus.FAILED]:
raise HTTPException(
status_code=400,
detail=f"Cannot cancel condition in {condition.status} status",
)

condition = completion_condition_service.update_status(
db, condition_id=condition_id, status=ConditionStatus.CANCELLED
)
return {"status": "cancelled", "id": condition_id}
Comment on lines +84 to +108
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move import to module level.

The ConditionStatus import on line 91 should be at the top of the file with other imports for consistency and to avoid repeated import overhead on each request.

 from app.schemas.completion_condition import (
     CompletionConditionCreate,
     CompletionConditionInDB,
     CompletionConditionListResponse,
     TaskCompletionStatus,
 )
+from app.models.completion_condition import ConditionStatus
 from app.services.completion_condition import completion_condition_service
 @router.delete("/{condition_id}/cancel")
 def cancel_completion_condition(
     condition_id: int,
     db: Session = Depends(get_db),
     current_user: User = Depends(security.get_current_user),
 ):
     """Cancel a completion condition"""
-    from app.models.completion_condition import ConditionStatus
-
     condition = completion_condition_service.get_by_id(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.delete("/{condition_id}/cancel")
def cancel_completion_condition(
condition_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Cancel a completion condition"""
from app.models.completion_condition import ConditionStatus
condition = completion_condition_service.get_by_id(
db, condition_id=condition_id, user_id=current_user.id
)
if not condition:
raise HTTPException(status_code=404, detail="Completion condition not found")
if condition.status in [ConditionStatus.SATISFIED, ConditionStatus.FAILED]:
raise HTTPException(
status_code=400,
detail=f"Cannot cancel condition in {condition.status} status",
)
condition = completion_condition_service.update_status(
db, condition_id=condition_id, status=ConditionStatus.CANCELLED
)
return {"status": "cancelled", "id": condition_id}
@router.delete("/{condition_id}/cancel")
def cancel_completion_condition(
condition_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Cancel a completion condition"""
condition = completion_condition_service.get_by_id(
db, condition_id=condition_id, user_id=current_user.id
)
if not condition:
raise HTTPException(status_code=404, detail="Completion condition not found")
if condition.status in [ConditionStatus.SATISFIED, ConditionStatus.FAILED]:
raise HTTPException(
status_code=400,
detail=f"Cannot cancel condition in {condition.status} status",
)
condition = completion_condition_service.update_status(
db, condition_id=condition_id, status=ConditionStatus.CANCELLED
)
return {"status": "cancelled", "id": condition_id}
🧰 Tools
🪛 Ruff (0.14.6)

87-87: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


88-88: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In backend/app/api/endpoints/completion_conditions.py around lines 84 to 108,
the code does a local import of ConditionStatus inside the
cancel_completion_condition function; move "from app.models.completion_condition
import ConditionStatus" to the module-level imports at the top of the file
alongside the other imports and remove the in-function import so the function
uses the top-level name, keeping import order consistent with the file's import
grouping.



@router.get("/tasks/{task_id}/completion-status", response_model=TaskCompletionStatus)
def get_task_completion_status(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""
Get the overall completion status for a task,
including all completion conditions and their status.
"""
status = completion_condition_service.get_task_completion_status(
db, task_id=task_id, user_id=current_user.id
)

# Convert conditions to schema objects
from app.schemas.completion_condition import CompletionConditionInDB

conditions_in_db = [
CompletionConditionInDB.model_validate(c) for c in status["conditions"]
]
Comment on lines +125 to +130
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move import to module level.

Same issue as above—the CompletionConditionInDB import on line 126 is already imported at the top of the file (line 18), making this local import redundant.

-    # Convert conditions to schema objects
-    from app.schemas.completion_condition import CompletionConditionInDB
-
     conditions_in_db = [
         CompletionConditionInDB.model_validate(c) for c in status["conditions"]
     ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Convert conditions to schema objects
from app.schemas.completion_condition import CompletionConditionInDB
conditions_in_db = [
CompletionConditionInDB.model_validate(c) for c in status["conditions"]
]
conditions_in_db = [
CompletionConditionInDB.model_validate(c) for c in status["conditions"]
]
🤖 Prompt for AI Agents
In backend/app/api/endpoints/completion_conditions.py around lines 125 to 130,
there is a redundant local import of CompletionConditionInDB that is already
imported at the top of the module; remove the local import statement and rely on
the existing top-level import so the list comprehension uses the module-level
CompletionConditionInDB, then run linters/tests to ensure no unused-import
warnings or name errors remain.


return TaskCompletionStatus(
task_id=task_id,
subtask_completed=True, # This would need to be checked from subtask status
all_conditions_satisfied=status["all_conditions_satisfied"],
pending_conditions=status["pending_conditions"],
in_progress_conditions=status["in_progress_conditions"],
satisfied_conditions=status["satisfied_conditions"],
failed_conditions=status["failed_conditions"],
conditions=conditions_in_db,
Comment on lines +132 to +140
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded subtask_completed=True may return incorrect status.

The comment acknowledges this needs to be checked from subtask status. Returning a hardcoded True could mislead API consumers into thinking a task is further along than it actually is.

Would you like me to generate an implementation that fetches the actual subtask completion status, or should I open an issue to track this as a follow-up?

🤖 Prompt for AI Agents
In backend/app/api/endpoints/completion_conditions.py around lines 132-140, the
code returns subtask_completed=True hardcoded; replace that with a real
computation that checks subtask status. Query the DB (or use the existing status
payload) to fetch subtask records for the given task_id, determine whether the
relevant subtask(s) are completed (e.g., all subtasks have status "COMPLETED" or
the specific subtask has status "COMPLETED"), set subtask_completed accordingly,
await the DB call if async, and default to False when no subtask records exist
or on error; update the returned TaskCompletionStatus to use this computed value
instead of the literal True.

)
11 changes: 11 additions & 0 deletions backend/app/api/endpoints/webhooks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""
Webhooks package for handling external CI events
"""
from app.api.endpoints.webhooks.github import router as github_router
from app.api.endpoints.webhooks.gitlab import router as gitlab_router

__all__ = ["github_router", "gitlab_router"]
Loading
Loading