feat. rebuild service in Go
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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user