4dd67742f9
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (/anthropic/v1, anthropic--claude-4.6-sonnet, sonnet, anthropic, SONNET_REVIEW_TOKEN) (pull_request) Successful in 43s
CI / review (/openai/v1, gpt-5, gpt, openai, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m28s
CI / review (/openai/v1, gpt-5, security, openai, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m55s
MAJOR fixes: - Remove external YAML dependency (github.com/goccy/go-yaml) Per project convention: Go standard library only, zero dependencies. Convert all persona files from YAML to JSON format. - Fix TestValidateWorkspacePath error expectation Go 1.21+ filepath.Join normalizes absolute paths differently. MINOR fixes: - Remove custom contains helper in persona_test.go (use strings.Contains) - Add Unicode-safe CapitalizeFirst function for header titles - ListBuiltinPersonas returns empty slice instead of nil on error - Fix test comment about filepath.Join behavior Documentation: - Update README to reflect JSON-only persona format - Update design doc with note about JSON decision - Fix action.yml description for persona-file input
71 lines
2.1 KiB
Go
71 lines
2.1 KiB
Go
package review
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// FormatMarkdown formats a ReviewResult into the markdown body for a Gitea review.
|
|
func FormatMarkdown(result *ReviewResult, reviewerName string) string {
|
|
return FormatMarkdownWithDisplay(result, reviewerName, reviewerName)
|
|
}
|
|
|
|
// GiteaEvent converts the verdict to the Gitea API event string.
|
|
func GiteaEvent(verdict string) string {
|
|
switch verdict {
|
|
case "APPROVE":
|
|
return "APPROVED"
|
|
case "REQUEST_CHANGES":
|
|
return "REQUEST_CHANGES"
|
|
default:
|
|
return "COMMENT"
|
|
}
|
|
}
|
|
|
|
// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.
|
|
// Note: displayName is not HTML-escaped as Gitea sanitizes rendered Markdown.
|
|
// Persona display names are controlled by repo owners (trusted input).
|
|
// displayName is used for the header title, sentinelName is used for the cleanup sentinel.
|
|
// If displayName is empty, sentinelName is used for both.
|
|
func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {
|
|
var sb strings.Builder
|
|
|
|
// Use display name for header, or fall back to sentinel name
|
|
headerName := displayName
|
|
if headerName == "" {
|
|
headerName = sentinelName
|
|
}
|
|
|
|
if headerName != "" {
|
|
title := CapitalizeFirst(headerName)
|
|
sb.WriteString(fmt.Sprintf("# %s Review\n\n", title))
|
|
}
|
|
|
|
sb.WriteString("## Summary\n\n")
|
|
sb.WriteString(result.Summary)
|
|
sb.WriteString("\n\n")
|
|
|
|
if len(result.Findings) > 0 {
|
|
sb.WriteString("## Findings\n\n")
|
|
sb.WriteString("| # | Severity | File | Line | Finding |\n")
|
|
sb.WriteString("|---|----------|------|------|--------|\n")
|
|
|
|
for i, f := range result.Findings {
|
|
sb.WriteString(fmt.Sprintf("| %d | [%s] | `%s` | %d | %s |\n",
|
|
i+1, f.Severity, f.File, f.Line, f.Finding))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("## Recommendation\n\n")
|
|
sb.WriteString(fmt.Sprintf("**%s** — %s\n", result.Verdict, result.Recommendation))
|
|
|
|
if sentinelName != "" {
|
|
sb.WriteString(fmt.Sprintf("\n---\n*Review by %s*\n", headerName))
|
|
// Hidden sentinel for identifying this bot's reviews during cleanup
|
|
sb.WriteString(fmt.Sprintf("\n<!-- review-bot:%s -->\n", sentinelName))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|