First MVP
This commit is contained in:
135
src/gitea_codex_bot/workers/dispatcher.py
Normal file
135
src/gitea_codex_bot/workers/dispatcher.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from gitea_codex_bot.config import Settings
|
||||
from gitea_codex_bot.db import get_session_factory
|
||||
from gitea_codex_bot.models import ReviewJob
|
||||
from gitea_codex_bot.services.comments import get_persistent_review_comment_id, upsert_persistent_review_comment_id
|
||||
from gitea_codex_bot.services.gitea import GiteaClient
|
||||
from gitea_codex_bot.services.jobs import claim_next_job, finish_job
|
||||
from gitea_codex_bot.services.review_format import format_result_comment
|
||||
from gitea_codex_bot.services.reviewer import create_fix_branch, create_fix_patch_note
|
||||
from gitea_codex_bot.types import ParsedCommand
|
||||
from gitea_codex_bot.workers.container_runner import run_review_ephemeral
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _command_from_job(job: ReviewJob) -> ParsedCommand:
|
||||
args = job.command_args.split() if job.command_args else []
|
||||
return ParsedCommand(name=job.command, raw=f"@codex {job.command}", arguments=args, full="--full" in args, branch_fix="--branch" in args)
|
||||
|
||||
|
||||
def _handle_non_review_command(
|
||||
settings: Settings,
|
||||
session: Session,
|
||||
gitea: GiteaClient,
|
||||
job: ReviewJob,
|
||||
command: ParsedCommand,
|
||||
) -> tuple[bool, bool, dict[str, Any] | None, str | None]:
|
||||
if command.name == "ignore":
|
||||
return True, True, {"summary": "Ignore command acknowledged. No review run executed."}, None
|
||||
if command.name == "explain":
|
||||
latest_review_job = session.execute(
|
||||
select(ReviewJob)
|
||||
.where(
|
||||
ReviewJob.repo == job.repo,
|
||||
ReviewJob.pr_number == job.pr_number,
|
||||
ReviewJob.command.in_(["review", "rerun"]),
|
||||
ReviewJob.status == "succeeded",
|
||||
)
|
||||
.order_by(ReviewJob.id.desc())
|
||||
.limit(1)
|
||||
).scalar_one_or_none()
|
||||
if latest_review_job and latest_review_job.result_json:
|
||||
message = f"## Codex Explain\n\n{latest_review_job.result_json.get('summary', 'No previous summary available.')}"
|
||||
else:
|
||||
message = "## Codex Explain\n\nNo previous result found for this command."
|
||||
gitea.post_issue_comment(job.repo, job.pr_number, message)
|
||||
return True, True, {"summary": message}, None
|
||||
if command.name == "fix":
|
||||
if not settings.enable_fix_commands:
|
||||
message = "⚠️ `@codex fix` is disabled on this bot instance."
|
||||
gitea.post_issue_comment(job.repo, job.pr_number, message)
|
||||
return True, True, {"summary": message}, None
|
||||
note = create_fix_patch_note(command)
|
||||
if command.branch_fix:
|
||||
try:
|
||||
pr = gitea.get_pull_request(job.repo, job.pr_number)
|
||||
branch = create_fix_branch(pr, note=note, arguments=command.arguments)
|
||||
message = f"## Codex Fix\n\n{note}\n\nCreated branch `{branch}`."
|
||||
gitea.post_issue_comment(job.repo, job.pr_number, message)
|
||||
return True, True, {"summary": note, "mode": "branch", "branch": branch}, None
|
||||
except Exception as exc:
|
||||
return True, False, None, f"Failed to create fix branch: {exc}"
|
||||
gitea.post_issue_comment(job.repo, job.pr_number, f"## Codex Fix\n\n{note}\n\nPatch suggestion mode.")
|
||||
return True, True, {"summary": note, "mode": "patch"}, None
|
||||
return False, False, None, None
|
||||
|
||||
|
||||
def process_one_job(settings: Settings) -> bool:
|
||||
session_factory = get_session_factory()
|
||||
with session_factory() as session:
|
||||
job = claim_next_job(session)
|
||||
if not job:
|
||||
return False
|
||||
|
||||
command = _command_from_job(job)
|
||||
gitea = GiteaClient(settings)
|
||||
|
||||
with session_factory() as session:
|
||||
db_job = session.execute(select(ReviewJob).where(ReviewJob.id == job.id)).scalar_one()
|
||||
handled, skipped, result, error = _handle_non_review_command(settings, session, gitea, db_job, command)
|
||||
if handled:
|
||||
finish_job(session, job_id=db_job.id, success=error is None, skipped=skipped, result=result, error_message=error)
|
||||
return True
|
||||
|
||||
try:
|
||||
pr_ctx = gitea.get_pull_request(job.repo, job.pr_number)
|
||||
if pr_ctx.is_fork and not settings.allow_untrusted_forks:
|
||||
with session_factory() as session:
|
||||
skip_message = "Skipped review for fork PR because `ALLOW_UNTRUSTED_FORKS=false`."
|
||||
gitea.post_issue_comment(job.repo, job.pr_number, skip_message)
|
||||
finish_job(
|
||||
session,
|
||||
job_id=job.id,
|
||||
success=True,
|
||||
skipped=True,
|
||||
result={"summary": skip_message},
|
||||
error_message=None,
|
||||
)
|
||||
return True
|
||||
result = run_review_ephemeral(settings, repo=job.repo, pr_number=job.pr_number, command=command)
|
||||
comment_body = format_result_comment(job.head_sha, result)
|
||||
with session_factory() as session:
|
||||
comment_id = get_persistent_review_comment_id(session, job.repo, job.pr_number)
|
||||
if comment_id:
|
||||
gitea.edit_issue_comment(job.repo, comment_id, comment_body)
|
||||
else:
|
||||
comment_id = gitea.post_issue_comment(job.repo, job.pr_number, comment_body)
|
||||
upsert_persistent_review_comment_id(
|
||||
session,
|
||||
repo=job.repo,
|
||||
pr_number=job.pr_number,
|
||||
head_sha=job.head_sha,
|
||||
comment_id=comment_id,
|
||||
)
|
||||
finish_job(session, job_id=job.id, success=True, skipped=False, result=result, error_message=None)
|
||||
except Exception as exc:
|
||||
logger.exception("Review job failed id=%s", job.id)
|
||||
with session_factory() as session:
|
||||
finish_job(session, job_id=job.id, success=False, skipped=False, result=None, error_message=str(exc))
|
||||
return True
|
||||
|
||||
|
||||
async def worker_loop(settings: Settings, stop_event: asyncio.Event) -> None:
|
||||
while not stop_event.is_set():
|
||||
processed = await asyncio.to_thread(process_one_job, settings)
|
||||
if not processed:
|
||||
await asyncio.sleep(1.0)
|
||||
Reference in New Issue
Block a user