139 lines
3.9 KiB
Python
139 lines
3.9 KiB
Python
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,
|
|
trigger_comment_body: str | None,
|
|
requested_by: str,
|
|
command: ParsedCommand,
|
|
) -> ReviewJob:
|
|
job = ReviewJob(
|
|
repo=repo,
|
|
pr_number=pr_number,
|
|
head_sha=head_sha,
|
|
trigger_comment_id=trigger_comment_id,
|
|
trigger_comment_body=trigger_comment_body,
|
|
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()
|