85c0e735dc
Fix runner bootstrap and auth handling, preserve queued SHAs, make event/job acceptance atomic, fence stale runs, correct retries and prompts, add fake end-to-end coverage, and fix deployment defaults. Co-Authored-By: Claude <noreply@anthropic.com>
217 lines
6.9 KiB
Go
217 lines
6.9 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 ApplyServerMaxDiff(cfg *domain.RepoReviewConfig, serverMax int) {
|
||
if serverMax <= 0 {
|
||
return
|
||
}
|
||
if cfg.MaxDiffBytes <= 0 || cfg.MaxDiffBytes > serverMax {
|
||
cfg.MaxDiffBytes = serverMax
|
||
}
|
||
}
|
||
|
||
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 {
|
||
intent := commandIntent(cmd.Raw)
|
||
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.\nMaximum diff bytes: %d.\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.MaxDiffBytes, cfg.IncludeTests, tests, cmd.Full)
|
||
}
|
||
|
||
func commandIntent(raw string) string {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
aliasEnd := strings.IndexAny(raw, " \t\r\n")
|
||
if aliasEnd < 0 {
|
||
return ""
|
||
}
|
||
remainder := strings.TrimSpace(raw[aliasEnd:])
|
||
commandEnd := strings.IndexAny(remainder, " \t\r\n")
|
||
if commandEnd < 0 {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(remainder[commandEnd:])
|
||
}
|
||
|
||
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 = fallbackDetails(result)
|
||
} else if len(result.Findings) > 0 {
|
||
body += "\n\n---\n\n" + structuredDetails(result)
|
||
}
|
||
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 fallbackDetails(result domain.ReviewResult) string {
|
||
body := fmt.Sprintf("## Codex Review\n\nVerdict: `%s`\nConfidence: `%.2f`\n\n%s", result.Verdict, result.Confidence, result.Summary)
|
||
if len(result.Findings) == 0 {
|
||
return body + "\n\nNo blocking issues found."
|
||
}
|
||
return body + "\n\n" + structuredFindings(result.Findings)
|
||
}
|
||
|
||
func structuredDetails(result domain.ReviewResult) string {
|
||
return fmt.Sprintf("### Structured Findings\n\nVerdict: `%s`\nConfidence: `%.2f`\n\n%s\n\n%s", result.Verdict, result.Confidence, result.Summary, structuredFindings(result.Findings))
|
||
}
|
||
|
||
func structuredFindings(findings []domain.Finding) string {
|
||
var body strings.Builder
|
||
body.WriteString("Findings:")
|
||
for i, f := range findings {
|
||
suggestion := "n/a"
|
||
if f.Suggestion != nil && *f.Suggestion != "" {
|
||
suggestion = *f.Suggestion
|
||
}
|
||
fmt.Fprintf(&body, "\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)
|
||
}
|
||
return body.String()
|
||
}
|
||
|
||
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
|
||
}
|