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
+179
View File
@@ -0,0 +1,179 @@
package review
import (
"encoding/json"
"fmt"
"strings"
"gopkg.in/yaml.v3"
"gitea-codex-bot/internal/domain"
)
type rawConfig struct {
Enabled *bool `yaml:"enabled"`
Review struct {
DefaultMode string `yaml:"default_mode"`
MaxDiffBytes int `yaml:"max_diff_bytes"`
IncludeTests bool `yaml:"include_tests"`
Focus []string `yaml:"focus"`
} `yaml:"review"`
Ignore []string `yaml:"ignore"`
}
func ParseRepoConfig(text string) (domain.RepoReviewConfig, error) {
cfg := domain.DefaultRepoReviewConfig()
cfg.Configured = true
var raw rawConfig
if err := yaml.Unmarshal([]byte(text), &raw); err != nil {
return domain.RepoReviewConfig{}, err
}
if raw.Enabled != nil {
cfg.Enabled = *raw.Enabled
}
if raw.Review.DefaultMode != "" {
cfg.DefaultMode = strings.ToLower(strings.TrimSpace(raw.Review.DefaultMode))
}
if raw.Review.MaxDiffBytes > 0 {
cfg.MaxDiffBytes = raw.Review.MaxDiffBytes
}
cfg.IncludeTests = raw.Review.IncludeTests
if raw.Review.Focus != nil {
cfg.Focus = boundedStrings(raw.Review.Focus, 32, 200)
}
cfg.Ignore = boundedStrings(raw.Ignore, 128, 500)
return cfg, nil
}
func MissingRepoConfig() domain.RepoReviewConfig {
cfg := domain.DefaultRepoReviewConfig()
cfg.Configured = false
return cfg
}
func boundedStrings(input []string, maxItems, maxLen int) []string {
out := make([]string, 0, min(len(input), maxItems))
for _, item := range input {
item = strings.TrimSpace(item)
if item != "" && len(item) <= maxLen {
out = append(out, item)
}
if len(out) == maxItems {
break
}
}
return out
}
func ResolveMode(cmd *domain.ParsedCommand, cfg domain.RepoReviewConfig) {
if cmd.Name == "review" && !cmd.ModeExplicit {
if cfg.DefaultMode == "full" || cfg.DefaultMode == "summary" || cfg.DefaultMode == "security" || cfg.DefaultMode == "performance" || cfg.DefaultMode == "tests" {
cmd.Mode = cfg.DefaultMode
} else {
cmd.Mode = "summary"
}
}
}
func BuildPrompt(cmd domain.ParsedCommand, cfg domain.RepoReviewConfig, pr domain.PullRequestContext) string {
raw := strings.TrimSpace(cmd.Raw)
intent := raw
if at := strings.IndexAny(raw, " \t\r\n"); at >= 0 {
intent = strings.TrimSpace(raw[at:])
}
if at := strings.IndexAny(intent, " \t\r\n"); at >= 0 {
intent = strings.TrimSpace(intent[at:])
}
if intent == "" {
intent = "review this pull request and report introduced issues."
}
focus := strings.Join(cfg.Focus, ", ")
if focus == "" {
focus = "correctness, security, maintainability"
}
ignore := strings.Join(cfg.Ignore, ", ")
if ignore == "" {
ignore = "(none)"
}
tests := "Do not run tests, benchmarks, or other executables. Review changes statically unless explicitly asked."
if cmd.Mode == "tests" || cfg.IncludeTests {
tests = "Tests may be executed for this run because tests mode/include_tests is explicitly enabled."
}
return fmt.Sprintf("review: %s\nReview only issues introduced by this PR.\nCompare exactly these commits: base `%s` ... head `%s`.\nUse local git data from this checkout; do not review unrelated history.\nRequested mode: %s.\nFocus areas: %s.\nIgnore patterns: %s.\nInclude tests setting: %t.\n%s\nFull review requested: %t.\nReturn strict JSON matching the provided output schema.", intent, pr.BaseSHA, pr.HeadSHA, cmd.Mode, focus, ignore, cfg.IncludeTests, tests, cmd.Full)
}
func ValidateResult(result domain.ReviewResult) error { return result.Validate() }
func DecodeResult(data []byte) (domain.ReviewResult, error) {
var result domain.ReviewResult
dec := json.NewDecoder(strings.NewReader(string(data)))
dec.DisallowUnknownFields()
if err := dec.Decode(&result); err != nil {
return domain.ReviewResult{}, err
}
if err := result.Validate(); err != nil {
return domain.ReviewResult{}, err
}
return result, nil
}
func FormatQueueAck(sha string) string {
return fmt.Sprintf("👀 Codex review queued for commit `%s`.", first(sha, 7))
}
func FormatCooldownAck(seconds int) string {
return fmt.Sprintf("⏳ Cooldown active. Please wait %ds before requesting another review on this PR.", seconds)
}
func FormatDisabledAck() string {
return "🚫 Review is disabled by `.codex-review.yml` for this repository."
}
func FormatUnsupportedAck(name string) string {
return fmt.Sprintf("⚠️ Command `@codex %s` is not enabled on this repository.", name)
}
func FormatResultComment(sha string, result domain.ReviewResult, configured bool) string {
marker := fmt.Sprintf("<!-- codex-review:head_sha=%s -->", sha)
body := strings.TrimSpace(result.MarkdownComment)
if body == "" {
body = fmt.Sprintf("## Codex Review\n\nVerdict: `%s`\nConfidence: `%.2f`\n\n%s", result.Verdict, result.Confidence, result.Summary)
if len(result.Findings) == 0 {
body += "\n\nNo blocking issues found."
} else {
body += "\n\nFindings:"
for i, f := range result.Findings {
suggestion := "n/a"
if f.Suggestion != nil && *f.Suggestion != "" {
suggestion = *f.Suggestion
}
body += fmt.Sprintf("\n\n%d. `%s:%d-%d` (%s)\n %s\n %s\n Suggestion: %s", i+1, f.File, f.LineStart, f.LineEnd, f.Severity, f.Title, f.Body, suggestion)
}
}
} else if len(result.Findings) > 0 {
body += "\n\n---\n\n### Structured Findings\n\n"
for i, f := range result.Findings {
body += fmt.Sprintf("%d. `%s:%d-%d` (%s)\n %s\n %s\n\n", i+1, f.File, f.LineStart, f.LineEnd, f.Severity, f.Title, f.Body)
}
}
if result.Meta != nil && result.Meta.Model != "" {
body += fmt.Sprintf("\n_Note: model `%s`, input `%d`, output `%d`, total `%d` tokens used._", result.Meta.Model, result.Meta.Usage.InputTokens, result.Meta.Usage.OutputTokens, result.Meta.Usage.TotalTokens)
}
if !configured {
body += "\n\n> ️.codex-review.yml is not configured"
}
if strings.HasPrefix(body, "<!-- codex-review:head_sha=") {
lines := strings.SplitN(body, "\n", 2)
if len(lines) == 2 {
body = marker + "\n" + lines[1]
}
} else {
body = marker + "\n" + body
}
return body
}
func FailureComment(sha, errText string) string {
return fmt.Sprintf("⚠️ Codex review run failed after queueing.\n\n- Commit: `%s`\n- Error: `%s`\n\nPlease rerun `@codex rerun` after checking worker logs.", first(sha, 7), first(strings.Join(strings.Fields(errText), " "), 500))
}
func first(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+45
View File
@@ -0,0 +1,45 @@
package review
import (
"testing"
"gitea-codex-bot/internal/domain"
)
func TestParseConfigAndResolveMode(t *testing.T) {
cfg, err := ParseRepoConfig("enabled: true\nreview:\n default_mode: security\n include_tests: false\n focus: [correctness, security]\nignore: [generated/]\n")
if err != nil {
t.Fatal(err)
}
if !cfg.Configured || cfg.DefaultMode != "security" || len(cfg.Focus) != 2 {
t.Fatalf("unexpected config: %#v", cfg)
}
cmd := domain.ParsedCommand{Name: "review", Raw: "@codex review", Mode: "summary"}
ResolveMode(&cmd, cfg)
if cmd.Mode != "security" {
t.Fatalf("mode was not resolved: %q", cmd.Mode)
}
}
func TestResultValidationAndFormatting(t *testing.T) {
suggestion := "Use a checked conversion."
result := domain.ReviewResult{Verdict: "has_issues", Confidence: .9, Summary: "Found one issue", Findings: []domain.Finding{{Severity: "high", File: "internal/x.go", LineStart: 4, LineEnd: 5, Title: "Unsafe conversion", Body: "The conversion can overflow.", Suggestion: &suggestion}}}
if err := ValidateResult(result); err != nil {
t.Fatal(err)
}
body := FormatResultComment("abcdef123", result, false)
if len(body) == 0 || body[:len("<!-- codex-review:head_sha=abcdef123 -->")] != "<!-- codex-review:head_sha=abcdef123 -->" {
t.Fatalf("missing SHA marker: %s", body)
}
if !contains(body, "not configured") || !contains(body, "Unsafe conversion") {
t.Fatalf("missing formatted details: %s", body)
}
}
func contains(s, needle string) bool {
for i := 0; i+len(needle) <= len(s); i++ {
if s[i:i+len(needle)] == needle {
return true
}
}
return false
}