[fix]. Restore PR-scoped review + remove fix cmd
All checks were successful
ci / test (pull_request) Successful in 32s
ci / publish (pull_request) Has been skipped

This commit is contained in:
Space-Banane
2026-05-23 14:15:00 +02:00
parent 9392591429
commit 08075cb3c4
14 changed files with 155 additions and 131 deletions

View File

@@ -40,7 +40,6 @@ class Settings(BaseSettings):
concurrency: int = Field(default=1, alias="CONCURRENCY")
review_runner_image: str = Field(default="node:22-bookworm-slim", alias="REVIEW_RUNNER_IMAGE")
enable_fix_commands: bool = Field(default=False, alias="ENABLE_FIX_COMMANDS")
allow_untrusted_forks: bool = Field(default=False, alias="ALLOW_UNTRUSTED_FORKS")
@field_validator("gitea_base_url")

View File

@@ -495,7 +495,7 @@ async def gitea_webhook(
gitea.post_issue_comment(repo, pr_number, format_queue_ack(head_sha))
return {"accepted": True, "job_id": job.id, "status": "queued"}
if parsed_command.name in {"fix", "explain", "ignore", "help"}:
if parsed_command.name in {"explain", "ignore", "help"}:
job = enqueue_job(
session,
repo=repo,

View File

@@ -7,7 +7,7 @@ from gitea_codex_bot.types import ParsedCommand
PREFIX_RE = re.compile(r"^@([^\s]+)\s+(.+)$", re.IGNORECASE | re.DOTALL)
HELP_ALIASES = {"-h", "--help", "help"}
SUPPORTED_COMMANDS = {"review", "explain", "fix", "ignore", "rerun"}
SUPPORTED_COMMANDS = {"review", "explain", "ignore", "rerun"}
def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedCommand | None:
@@ -44,6 +44,4 @@ def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedComm
parsed.mode = mode
parsed.mode_explicit = True
break
elif name == "fix":
parsed.branch_fix = "--branch" in tokens
return parsed

View File

@@ -1,32 +1,12 @@
from __future__ import annotations
import shlex
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
from gitea_codex_bot.services.gitea import PullRequestContext
from gitea_codex_bot.types import ParsedCommand
class ReviewError(RuntimeError):
pass
def _run_git(args: list[str], cwd: Path | None = None) -> str:
completed = subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True)
return completed.stdout
def checkout_pr(tmpdir: Path, pr: PullRequestContext) -> Path:
repo_dir = tmpdir / "repo"
_run_git(["clone", "--no-tags", "--depth", "50", pr.clone_url, str(repo_dir)])
_run_git(["fetch", "origin", pr.base_ref, pr.head_ref], cwd=repo_dir)
_run_git(["checkout", pr.head_sha], cwd=repo_dir)
return repo_dir
def normalize_review_result(result: Any) -> dict[str, Any]:
if not isinstance(result, dict):
raise ReviewError(f"Invalid review result type: {type(result)!r}")
@@ -39,44 +19,3 @@ def normalize_review_result(result: Any) -> dict[str, Any]:
if "confidence" not in result:
result["confidence"] = 0.5
return result
def summarize_command(command: ParsedCommand) -> str:
return " ".join(["@codex", command.name, *command.arguments]).strip()
def fix_branch_name(pr_number: int, arguments: list[str] | None = None) -> str:
suffix = "fix"
if arguments:
words = [token.lower().strip() for token in arguments if token.strip() and not token.startswith("--")]
if words:
clean = "-".join(words[:4])
cleaned = "".join(ch if ch.isalnum() or ch == "-" else "-" for ch in clean).strip("-")
if cleaned:
suffix = f"fix-{cleaned}"
return f"codex/pr-{pr_number}-{suffix}"
def create_fix_patch_note(command: ParsedCommand) -> str:
details = shlex.join(command.arguments) if command.arguments else "latest findings"
return f"Fix command requested for {details}."
def create_fix_branch(
pr: PullRequestContext,
*,
note: str,
arguments: list[str] | None = None,
) -> str:
branch = fix_branch_name(pr.pr_number, arguments=arguments)
with TemporaryDirectory(prefix="gitea-codex-fix-") as tmp:
tmpdir = Path(tmp)
repo_dir = checkout_pr(tmpdir, pr)
_run_git(["checkout", "-b", branch], cwd=repo_dir)
notes_dir = repo_dir / ".codex"
notes_dir.mkdir(parents=True, exist_ok=True)
(notes_dir / "fix-note.md").write_text(f"# Codex Fix Note\n\n{note}\n", encoding="utf-8")
_run_git(["add", ".codex/fix-note.md"], cwd=repo_dir)
_run_git(["-c", "user.name=codex-bot", "-c", "user.email=codex-bot@example.invalid", "commit", "-m", f"Codex fix note for PR {pr.pr_number}"], cwd=repo_dir)
_run_git(["push", "origin", f"{branch}:{branch}", "--force"], cwd=repo_dir)
return branch

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
from typing import Literal
CommandName = Literal["review", "explain", "fix", "ignore", "rerun", "help"]
CommandName = Literal["review", "explain", "ignore", "rerun", "help"]
@dataclass(slots=True)
@@ -14,5 +14,4 @@ class ParsedCommand:
mode: str = "summary"
mode_explicit: bool = False
full: bool = False
branch_fix: bool = False
arguments: list[str] = field(default_factory=list)

View File

@@ -64,8 +64,12 @@ def run_review_ephemeral(
gitea = GiteaClient(settings)
pr = gitea.get_pull_request(repo, pr_number)
repo_cfg = _load_repo_review_config_from_gitea(gitea, repo, pr.head_sha)
review_prompt = _build_exec_review_prompt(command)
_apply_repo_default_review_mode(command, repo_cfg)
review_prompt = _build_exec_review_prompt(command, repo_cfg, pr)
container_name = f"codex-review-{uuid.uuid4().hex[:12]}"
marker_nonce = uuid.uuid4().hex
result_start_marker = f"{RESULT_START_MARKER}_{marker_nonce}"
result_end_marker = f"{RESULT_END_MARKER}_{marker_nonce}"
extra_env: dict[str, str] = {
"GITEA_TOKEN": settings.gitea_token.get_secret_value(),
"GITEA_GIT_USERNAME": settings.gitea_bot_username,
@@ -84,11 +88,17 @@ def run_review_ephemeral(
pr=pr,
container_name=container_name,
review_prompt=review_prompt,
result_start_marker=result_start_marker,
result_end_marker=result_end_marker,
extra_env=extra_env,
)
if completed.returncode != 0:
raise RuntimeError(_format_runner_failure(completed))
parsed = _parse_review_result_from_stdout_artifact(completed.stdout)
parsed = _parse_review_result_from_stdout_artifact(
completed.stdout,
result_start_marker=result_start_marker,
result_end_marker=result_end_marker,
)
parsed["_meta"] = _extract_result_meta_from_codex_stdout(completed.stdout, settings)
return normalize_review_result(parsed), repo_cfg
except Exception as exc:
@@ -102,9 +112,17 @@ def _run_ephemeral_container(
pr: PullRequestContext,
container_name: str,
review_prompt: str,
result_start_marker: str,
result_end_marker: str,
extra_env: dict[str, str],
) -> subprocess.CompletedProcess[str]:
install_and_run = _build_install_and_run_command(settings, pr=pr, review_prompt=review_prompt)
install_and_run = _build_install_and_run_command(
settings,
pr=pr,
review_prompt=review_prompt,
result_start_marker=result_start_marker,
result_end_marker=result_end_marker,
)
cmd = _build_docker_command(settings, container_name=container_name, install_and_run=install_and_run)
return subprocess.run(
cmd,
@@ -121,6 +139,8 @@ def _build_install_and_run_command(
*,
pr: PullRequestContext,
review_prompt: str,
result_start_marker: str,
result_end_marker: str,
) -> str:
steps = ["set -euo pipefail"]
if settings.codex_auth_mode != "chatgpt":
@@ -190,23 +210,45 @@ def _build_install_and_run_command(
steps.extend(
[
" ".join(codex_exec_parts),
f'echo "{RESULT_START_MARKER}"',
f'echo "{result_start_marker}"',
f"cat {shlex.quote(REVIEW_OUTPUT_FILE)}",
f'echo "{RESULT_END_MARKER}"',
f'echo "{result_end_marker}"',
]
)
return "\n".join(steps)
def _build_exec_review_prompt(command: ParsedCommand) -> str:
def _apply_repo_default_review_mode(command: ParsedCommand, repo_cfg: RepoReviewConfig) -> None:
if command.name != "review" or command.mode_explicit:
return
configured_mode = repo_cfg.default_mode
command.mode = configured_mode if configured_mode in {"summary", "security", "performance", "tests", "full"} else "summary"
def _build_exec_review_prompt(command: ParsedCommand, repo_cfg: RepoReviewConfig, pr: PullRequestContext) -> str:
raw = (command.raw or "").strip()
remainder = raw
match = re.match(r"^@[^\s]+\s+\S+\s*(.*)$", raw, flags=re.IGNORECASE | re.DOTALL)
if match:
remainder = match.group(1).strip()
if not remainder:
return "review: review this pull request and report introduced issues."
return f"review: {remainder}"
intent = remainder or "review this pull request and report introduced issues."
focus = ", ".join(repo_cfg.focus) if repo_cfg.focus else "correctness, security, maintainability"
ignore = ", ".join(repo_cfg.ignore) if repo_cfg.ignore else "(none)"
mode = command.mode if command.name in {"review", "rerun"} else "summary"
return "\n".join(
[
f"review: {intent}",
"Review only issues introduced by this PR.",
f"Compare exactly these commits: base `{pr.base_sha}` ... head `{pr.head_sha}`.",
"Use local git data from this checkout; do not review unrelated history.",
f"Requested mode: {mode}.",
f"Focus areas: {focus}.",
f"Ignore patterns: {ignore}.",
f"Include tests setting: {repo_cfg.include_tests}.",
f"Full review requested: {command.full}.",
"Return strict JSON matching the provided output schema.",
]
)
def _build_docker_command(settings: Settings, *, container_name: str, install_and_run: str) -> list[str]:
@@ -323,12 +365,27 @@ def _load_repo_review_config_from_gitea(gitea: GiteaClient, repo: str, head_sha:
return parse_repo_review_config_text(content, configured=True)
def _parse_review_result_from_stdout_artifact(stdout: str) -> dict[str, Any]:
start = stdout.find(RESULT_START_MARKER)
end = stdout.find(RESULT_END_MARKER)
if start == -1 or end == -1 or end <= start:
def _parse_review_result_from_stdout_artifact(
stdout: str,
*,
result_start_marker: str,
result_end_marker: str,
) -> dict[str, Any]:
lines = stdout.splitlines()
start_idx = -1
end_idx = -1
for idx, line in enumerate(lines):
if line.strip() == result_start_marker:
start_idx = idx
break
if start_idx != -1:
for idx in range(start_idx + 1, len(lines)):
if lines[idx].strip() == result_end_marker:
end_idx = idx
break
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
raise RuntimeError("Runner output did not include final review artifact markers.")
artifact = stdout[start + len(RESULT_START_MARKER) : end].strip()
artifact = "\n".join(lines[start_idx + 1 : end_idx]).strip()
if not artifact:
raise RuntimeError("Runner output contained empty final review artifact.")
try:

View File

@@ -14,7 +14,6 @@ from gitea_codex_bot.services.comments import upsert_persistent_review_comment_i
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_disabled_ack, 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
@@ -24,7 +23,7 @@ logger = logging.getLogger(__name__)
def _command_from_job(job: ReviewJob) -> ParsedCommand:
args = job.command_args.split() if job.command_args else []
raw = (job.trigger_comment_body or "").strip() or f"@codex {job.command}"
return ParsedCommand(name=job.command, raw=raw, arguments=args, full="--full" in args, branch_fix="--branch" in args)
return ParsedCommand(name=job.command, raw=raw, arguments=args, full="--full" in args)
def _handle_non_review_command(
@@ -61,23 +60,10 @@ def _handle_non_review_command(
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
if str(command.name).lower() == "fix":
message = "⚠️ `@codex fix` is no longer supported on this bot."
gitea.post_issue_comment(job.repo, job.pr_number, message)
return True, True, {"summary": message}, None
return False, False, None, None
@@ -120,13 +106,11 @@ def _build_help_comment(settings: Settings, session: Session, gitea: GiteaClient
"- `@codex review [security|performance|tests] [--full]`",
"- `@codex rerun`",
"- `@codex explain`",
"- `@codex fix [--branch ...]`",
"- `@codex ignore`",
"- `@codex -h` / `@codex --help` / `@codex help`",
"",
"Status note:",
f"- Pending jobs on this PR: `{pending_count}`",
f"- Fix command enabled: `{str(settings.enable_fix_commands).lower()}`",
f"- {latest_status_line}",
"",
f"Discussion summary ({comment_summaries['total']} comments, human `{comment_summaries['human']}`, bot `{comment_summaries['bot']}`):",

View File

@@ -17,7 +17,6 @@ def main() -> int:
raw=f"@codex {command_payload['name']}",
mode=command_payload.get("mode", "summary"),
full=bool(command_payload.get("full", False)),
branch_fix=bool(command_payload.get("branch_fix", False)),
arguments=list(command_payload.get("arguments", [])),
)
result, _repo_cfg = run_review_ephemeral(