f19b271642
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>
180 lines
6.2 KiB
Go
180 lines
6.2 KiB
Go
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
|
||
}
|