First MVP

This commit is contained in:
Space-Banane
2026-05-22 19:25:57 +02:00
parent 673f70b32a
commit 860ccb731d
40 changed files with 2336 additions and 0 deletions

View 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)