feat: sentinel-based review cleanup + system prompt file + security review
CI / test (pull_request) Successful in 14s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 23s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 58s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m35s

Sentinel-based cleanup:
- Reviews embed <!-- review-bot:NAME --> in body (hidden HTML comment)
- Cleanup matches by sentinel, not token identity
- Each reviewer-name is a logical identity (sonnet, gpt, security)
- Same token can run multiple review types without conflict
- No extra API scopes needed

System prompt file (--system-prompt-file / SYSTEM_PROMPT_FILE):
- Loads a local file with additional review instructions
- Appended to system base as "Additional Review Instructions"
- Enables specialized reviews (security, performance, etc.)
- Partially addresses #5

Security review:
- SECURITY_REVIEW.md prompt focused on vulnerabilities
- 3rd CI matrix entry using same token, different prompt
- Focus: injection, auth, secrets, input validation, crypto, races

CI changes:
- REVIEWER_NAME passed from matrix.name
- SYSTEM_PROMPT_FILE passed from matrix (empty for standard reviews)
- 3 reviewers: sonnet (general), gpt (general), security (focused)
This commit is contained in:
Rodin
2026-05-01 20:55:09 -07:00
parent 41c670b44b
commit 69e0a459c3
7 changed files with 71 additions and 4 deletions
+5
View File
@@ -70,6 +70,10 @@ inputs:
description: 'Delete previous review from same bot before posting. Accepts: true/1/yes or false/0/no (default true)'
required: false
default: 'true'
system-prompt-file:
description: 'Local file with additional system prompt instructions (e.g. security review focus)'
required: false
default: ''
runs:
using: 'composite'
@@ -150,6 +154,7 @@ runs:
LLM_TIMEOUT: ${{ inputs.timeout }}
LLM_PROVIDER: ${{ inputs.llm-provider }}
UPDATE_EXISTING: ${{ inputs.update-existing }}
SYSTEM_PROMPT_FILE: ${{ inputs.system-prompt-file }}
run: |
ARGS=""
if [ "${{ inputs.dry-run }}" = "true" ]; then
+6
View File
@@ -32,6 +32,10 @@ jobs:
- name: gpt
token_secret: GPT_REVIEW_TOKEN
model: gpt-4.1
- name: security
token_secret: SONNET_REVIEW_TOKEN
model: gpt-5
system_prompt_file: SECURITY_REVIEW.md
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
@@ -44,6 +48,7 @@ jobs:
GITEA_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEWER_TOKEN: ${{ secrets[matrix.token_secret] }}
REVIEWER_NAME: ${{ matrix.name }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_MODEL: ${{ matrix.model }}
@@ -51,4 +56,5 @@ jobs:
PATTERNS_REPO: "rodin/go-patterns"
PATTERNS_FILES: "README.md,patterns/"
LLM_TIMEOUT: "600"
SYSTEM_PROMPT_FILE: ${{ matrix.system_prompt_file }}
run: ./review-bot
+18
View File
@@ -0,0 +1,18 @@
You are performing a security-focused code review. Your primary concern is identifying vulnerabilities, not general code quality.
Focus areas:
- **Injection attacks**: SQL injection, command injection, path traversal, template injection
- **Authentication/Authorization**: Missing auth checks, privilege escalation, IDOR
- **Secrets exposure**: Hardcoded credentials, API keys in code, tokens in logs
- **Input validation**: Untrusted input used without sanitization, unsafe deserialization
- **Cryptography**: Weak algorithms, predictable randomness, improper key management
- **Error handling**: Information leakage in error messages, stack traces exposed
- **Dependencies**: Known vulnerable patterns, unsafe use of external libraries
- **Race conditions**: TOCTOU bugs, unsynchronized shared state
- **Resource exhaustion**: Unbounded allocations, missing timeouts, denial-of-service vectors
Rules for this review:
- Only report findings with actual security implications. Ignore style, naming, and general code quality.
- Severity mapping: MAJOR = exploitable vulnerability or data exposure. MINOR = defense-in-depth improvement or hardening opportunity. NIT = theoretical concern with low practical risk.
- If the code has no security-relevant changes, APPROVE with an empty findings list.
- Do not duplicate findings that a standard code review would catch (logic bugs, missing error checks) unless they have a security dimension.
+21 -4
View File
@@ -30,6 +30,7 @@ func main() {
llmAPIKey := flag.String("llm-api-key", envOrDefault("LLM_API_KEY", ""), "LLM API key")
llmModel := flag.String("llm-model", envOrDefault("LLM_MODEL", ""), "LLM model name")
conventionsFile := flag.String("conventions-file", envOrDefault("CONVENTIONS_FILE", ""), "Conventions file path in repo (e.g. CLAUDE.md)")
systemPromptFile := flag.String("system-prompt-file", envOrDefault("SYSTEM_PROMPT_FILE", ""), "Local file with additional system prompt instructions")
patternsRepo := flag.String("patterns-repo", envOrDefault("PATTERNS_REPO", ""), "Repo with language patterns (e.g. rodin/elixir-patterns)")
patternsFiles := flag.String("patterns-files", envOrDefault("PATTERNS_FILES", "README.md"), "Comma-separated file paths to fetch from patterns repo")
dryRun := flag.Bool("dry-run", false, "Print review to stdout instead of posting")
@@ -150,9 +151,24 @@ func main() {
log.Printf("Loaded patterns from %s (%d bytes)", *patternsRepo, len(patterns))
}
// Step 6b: Load additional system prompt if specified
additionalPrompt := ""
if *systemPromptFile != "" {
data, err := os.ReadFile(*systemPromptFile)
if err != nil {
log.Fatalf("Failed to read system prompt file %q: %v", *systemPromptFile, err)
}
additionalPrompt = string(data)
log.Printf("Loaded system prompt file: %s (%d bytes)", *systemPromptFile, len(additionalPrompt))
}
// Step 7: Budget-aware prompt assembly
systemBase := review.BuildSystemBase()
if additionalPrompt != "" {
systemBase += "\n\n## Additional Review Instructions\n\n" + additionalPrompt
}
sections := budget.Sections{
SystemBase: review.BuildSystemBase(),
SystemBase: systemBase,
Patterns: patterns,
Conventions: conventions,
FileContext: fileContext,
@@ -203,14 +219,15 @@ func main() {
}
log.Printf("Review posted (id=%d, user=%s)", posted.ID, posted.User.Login)
// Delete stale reviews from this bot if update-existing is enabled
if *updateExisting && posted.User.Login != "" {
// Delete stale reviews from this bot using sentinel matching
sentinel := fmt.Sprintf("<!-- review-bot:%s -->", *reviewerName)
if *updateExisting && *reviewerName != "" {
reviews, err := giteaClient.ListReviews(ctx, owner, repoName, prNumber)
if err != nil {
log.Printf("Warning: could not list existing reviews: %v", err)
} else {
for _, r := range reviews {
if r.User.Login == posted.User.Login && r.ID != posted.ID {
if r.ID != posted.ID && strings.Contains(r.Body, sentinel) {
if err := giteaClient.DeleteReview(ctx, owner, repoName, prNumber, r.ID); err != nil {
log.Printf("Warning: could not delete old review %d: %v", r.ID, err)
} else {
+1
View File
@@ -279,6 +279,7 @@ func (c *Client) GetAllFilesInPath(ctx context.Context, owner, repo, path string
// Review represents a pull request review from the Gitea API.
type Review struct {
ID int64 `json:"id"`
Body string `json:"body"`
User struct {
Login string `json:"login"`
} `json:"user"`
+2
View File
@@ -30,6 +30,8 @@ func FormatMarkdown(result *ReviewResult, reviewerName string) string {
if reviewerName != "" {
sb.WriteString(fmt.Sprintf("\n---\n*Review by %s*\n", reviewerName))
// Hidden sentinel for identifying this bot's reviews during cleanup
sb.WriteString(fmt.Sprintf("\n<!-- review-bot:%s -->\n", reviewerName))
}
return sb.String()
+18
View File
@@ -116,3 +116,21 @@ func TestGiteaEvent(t *testing.T) {
}
}
}
func TestFormatMarkdown_Sentinel(t *testing.T) {
result := &ReviewResult{
Verdict: "APPROVE",
Summary: "All good.",
Recommendation: "Merge it.",
}
output := FormatMarkdown(result, "security")
if !strings.Contains(output, "<!-- review-bot:security -->") {
t.Error("expected sentinel comment in output")
}
// Empty reviewer name should NOT have sentinel
output2 := FormatMarkdown(result, "")
if strings.Contains(output2, "<!-- review-bot") {
t.Error("should not contain sentinel when reviewer name is empty")
}
}