feat. rebuild service in Go
ci / test (pull_request) Successful in 1m54s
ci / publish (pull_request) Has been skipped

Rebuild the Gitea Codex review bot from the product contract with a Go HTTP service, durable SQL queue, typed Gitea client, isolated runner, and deployment updates.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Space-Banane
2026-07-12 21:51:01 +02:00
parent fdd3819ff8
commit f19b271642
74 changed files with 2623 additions and 4819 deletions
+150
View File
@@ -0,0 +1,150 @@
package runner
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"gitea-codex-bot/internal/config"
"gitea-codex-bot/internal/domain"
"gitea-codex-bot/internal/review"
)
const startMarker = "__CODEX_REVIEW_RESULT_BEGIN__"
const endMarker = "__CODEX_REVIEW_RESULT_END__"
const maxRunnerOutput = 4 << 20
type DockerRunner struct{ settings config.Settings }
func NewDockerRunner(settings config.Settings) *DockerRunner {
return &DockerRunner{settings: settings}
}
func (r *DockerRunner) Run(parent context.Context, pr domain.PullRequestContext, cmd domain.ParsedCommand, cfg domain.RepoReviewConfig) (domain.ReviewResult, error) {
ctx, cancel := context.WithTimeout(parent, time.Duration(r.settings.MaxReviewMinutes)*time.Minute)
defer cancel()
nonce := fmt.Sprintf("%d", time.Now().UnixNano())
begin, end := startMarker+"_"+nonce, endMarker+"_"+nonce
prompt := review.BuildPrompt(cmd, cfg, pr)
script := r.script(pr, prompt, begin, end)
name := "codex-review-" + nonce
args := []string{"run", "--rm", "-i", "--name", name, "--cap-drop=ALL", "--security-opt", "no-new-privileges", "--read-only", "--tmpfs", "/tmp:rw,noexec,nosuid,size=512m", "--tmpfs", "/work:rw,nosuid,size=1g", "-e", "CODEX_DISABLE_TELEMETRY=1"}
if r.settings.CodexAuthMode == "chatgpt" {
args = append(args, "-e", "CODEX_AUTH_JSON_B64")
} else {
args = append(args, "-e", "OPENAI_API_KEY")
}
args = append(args, "-e", "GITEA_TOKEN", "-e", "GITEA_GIT_USERNAME", r.settings.RunnerImage, "bash", "-lc", script)
cmdExec := exec.CommandContext(ctx, "docker", args...)
cmdExec.Env = append(os.Environ(), "OPENAI_API_KEY="+r.settings.OpenAIAPIKey, "GITEA_TOKEN="+r.settings.GiteaToken, "GITEA_GIT_USERNAME="+r.settings.GiteaBotUsername)
if r.settings.CodexAuthMode == "chatgpt" {
data, err := readAuthJSON(r.settings.CodexAuthJSONPath)
if err != nil {
return domain.ReviewResult{}, err
}
cmdExec.Env = append(cmdExec.Env, "CODEX_AUTH_JSON_B64="+base64.StdEncoding.EncodeToString(data))
}
var output limitedBuffer
output.limit = maxRunnerOutput
cmdExec.Stdout = &output
cmdExec.Stderr = &output
if err := cmdExec.Run(); err != nil {
_ = exec.CommandContext(context.Background(), "docker", "rm", "-f", name).Run()
if ctx.Err() != nil {
return domain.ReviewResult{}, fmt.Errorf("review runner timeout: %w", ctx.Err())
}
return domain.ReviewResult{}, fmt.Errorf("review runner failed: %w", err)
}
text := output.String()
start := strings.Index(text, begin)
endPos := strings.LastIndex(text, end)
if start < 0 || endPos <= start {
return domain.ReviewResult{}, fmt.Errorf("review runner returned no result artifact")
}
artifact := strings.TrimSpace(text[start+len(begin) : endPos])
var result domain.ReviewResult
if err := json.Unmarshal([]byte(artifact), &result); err != nil {
return domain.ReviewResult{}, err
}
if err := review.ValidateResult(result); err != nil {
return domain.ReviewResult{}, err
}
return result, nil
}
func readAuthJSON(rawPath string) ([]byte, error) {
path := os.ExpandEnv(rawPath)
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
path = filepath.Join(home, strings.TrimPrefix(path, "~/"))
}
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, err
}
if !json.Valid(data) {
return nil, fmt.Errorf("CODEX_AUTH_JSON_PATH is not valid JSON")
}
return data, nil
}
func (r *DockerRunner) script(pr domain.PullRequestContext, prompt, begin, end string) string {
auth := base64.StdEncoding.EncodeToString([]byte(r.settings.GiteaBotUsername + ":" + r.settings.GiteaToken))
schema := `{"type":"object","additionalProperties":false,"required":["verdict","confidence","summary","findings","markdown_comment"],"properties":{"verdict":{"type":"string","enum":["correct","has_issues"]},"confidence":{"type":"number"},"summary":{"type":"string"},"markdown_comment":{"type":"string"},"findings":{"type":"array"}}}`
quote := func(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'" }
baseRemote := "origin"
remoteSetup := ""
if pr.BaseCloneURL != "" && pr.BaseCloneURL != pr.CloneURL {
baseRemote = "upstream"
remoteSetup = "git remote add upstream " + quote(pr.BaseCloneURL)
}
authSetup := ""
if r.settings.CodexAuthMode == "chatgpt" {
authSetup = "mkdir -p /root/.codex; printf '%s' \"$CODEX_AUTH_JSON_B64\" | base64 -d > /root/.codex/auth.json; chmod 600 /root/.codex/auth.json"
}
fetchHead := "git -c http.extraHeader=" + quote("Authorization: Basic "+auth) + " fetch --no-tags origin " + quote(pr.HeadRef) + " || git -c http.extraHeader=" + quote("Authorization: Basic "+auth) + " fetch --no-tags origin " + quote(pr.HeadSHA)
fetchBase := "git -c http.extraHeader=" + quote("Authorization: Basic "+auth) + " fetch --no-tags " + baseRemote + " " + quote(pr.BaseRef) + " || git -c http.extraHeader=" + quote("Authorization: Basic "+auth) + " fetch --no-tags " + baseRemote + " " + quote(pr.BaseSHA)
steps := []string{"set -eu", "printf '%s' " + quote(schema) + " > /tmp/schema.json"}
if authSetup != "" {
steps = append(steps, authSetup)
}
steps = append(steps, "git -c http.extraHeader="+quote("Authorization: Basic "+auth)+" clone --no-tags --depth 80 "+quote(pr.CloneURL)+" /work/repo", "cd /work/repo")
if remoteSetup != "" {
steps = append(steps, remoteSetup)
}
steps = append(steps, fetchHead, fetchBase, "git checkout --detach "+quote(pr.HeadSHA), "test \"$(git rev-parse HEAD)\" = "+quote(pr.HeadSHA), "unset GITEA_TOKEN", "codex exec --sandbox danger-full-access --json --output-schema /tmp/schema.json -o /tmp/result.json -m "+quote(r.settings.OpenAIReviewModel)+" "+quote(prompt), "test -s /tmp/result.json", "printf '%s\\n' "+quote(begin), "cat /tmp/result.json", "printf '%s\\n' "+quote(end))
return strings.Join(steps, "; ")
}
type limitedBuffer struct {
buffer bytes.Buffer
limit int
truncated bool
}
func (b *limitedBuffer) Write(p []byte) (int, error) {
remaining := b.limit - b.buffer.Len()
if remaining <= 0 {
b.truncated = true
return len(p), nil
}
if len(p) > remaining {
_, _ = b.buffer.Write(p[:remaining])
b.truncated = true
return len(p), nil
}
return b.buffer.Write(p)
}
func (b *limitedBuffer) String() string { return b.buffer.String() }
var _ domain.ReviewRunner = (*DockerRunner)(nil)