First MVP
Some checks failed
ci / test (push) Failing after 12s
ci / publish (push) Has been skipped

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

View File

@@ -0,0 +1,136 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from gitea_codex_bot.models import JobStatus, ReviewJob, ReviewRun, RunStatus, WebhookEvent
from gitea_codex_bot.services.security import payload_digest
from gitea_codex_bot.types import ParsedCommand
def persist_webhook_event(
session: Session,
*,
delivery_id: str | None,
event_name: str,
repo: str,
comment_id: int | None,
payload: bytes,
) -> bool:
event = WebhookEvent(
delivery_id=delivery_id,
event_name=event_name,
repo=repo,
comment_id=comment_id,
payload_sha256=payload_digest(payload),
)
session.add(event)
try:
session.commit()
return True
except IntegrityError:
session.rollback()
return False
def cooldown_remaining_seconds(session: Session, repo: str, pr_number: int, cooldown_seconds: int) -> int:
cutoff = datetime.now(timezone.utc) - timedelta(seconds=cooldown_seconds)
row = session.execute(
select(ReviewJob)
.where(ReviewJob.repo == repo, ReviewJob.pr_number == pr_number, ReviewJob.created_at >= cutoff)
.order_by(ReviewJob.created_at.desc())
.limit(1)
).scalar_one_or_none()
if not row:
return 0
created_at = row.created_at
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
age = (datetime.now(timezone.utc) - created_at).total_seconds()
remaining = int(max(cooldown_seconds - age, 0))
return remaining
def enqueue_job(
session: Session,
*,
repo: str,
pr_number: int,
head_sha: str,
trigger_comment_id: int,
requested_by: str,
command: ParsedCommand,
) -> ReviewJob:
job = ReviewJob(
repo=repo,
pr_number=pr_number,
head_sha=head_sha,
trigger_comment_id=trigger_comment_id,
command=command.name,
command_args=" ".join(command.arguments) if command.arguments else None,
requested_by=requested_by,
status=JobStatus.queued,
)
session.add(job)
session.commit()
session.refresh(job)
return job
def claim_next_job(session: Session) -> ReviewJob | None:
job = session.execute(
select(ReviewJob).where(ReviewJob.status == JobStatus.queued).order_by(ReviewJob.created_at.asc()).limit(1).with_for_update(skip_locked=True)
).scalar_one_or_none()
if not job:
session.rollback()
return None
job.status = JobStatus.running
job.started_at = datetime.now(timezone.utc)
run = ReviewRun(job_id=job.id, status=RunStatus.running)
session.add(run)
session.commit()
session.refresh(job)
return job
def finish_job(
session: Session,
*,
job_id: int,
success: bool,
skipped: bool,
result: dict | None,
error_message: str | None,
) -> None:
job = session.get(ReviewJob, job_id)
if not job:
return
latest_run = (
session.execute(select(ReviewRun).where(ReviewRun.job_id == job_id).order_by(ReviewRun.id.desc()).limit(1)).scalar_one_or_none()
)
if skipped:
job.status = JobStatus.skipped
run_status = RunStatus.skipped
elif success:
job.status = JobStatus.succeeded
run_status = RunStatus.succeeded
else:
job.status = JobStatus.failed
run_status = RunStatus.failed
now = datetime.now(timezone.utc)
job.finished_at = now
job.last_error = error_message
if result is not None:
job.result_json = result
if latest_run:
latest_run.status = run_status
latest_run.finished_at = now
latest_run.result_json = result
latest_run.error_message = error_message
session.commit()