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>
152 lines
4.7 KiB
Go
152 lines
4.7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Settings struct {
|
|
GiteaBaseURL string
|
|
GiteaToken string
|
|
GiteaBotUsername string
|
|
GiteaBotMentions string
|
|
GiteaWebhookSecret string
|
|
OpenAIAPIKey string
|
|
OpenAIProjectID string
|
|
OpenAIOrgID string
|
|
OpenAIReviewModel string
|
|
CodexAuthMode string
|
|
CodexAuthJSONPath string
|
|
AllowedRepos []string
|
|
CooldownSeconds int
|
|
WebhookMode string
|
|
DatabaseURL string
|
|
DBHost string
|
|
DBPort int
|
|
DBName string
|
|
DBUser string
|
|
DBPassword string
|
|
Workdir string
|
|
MaxDiffBytes int
|
|
MaxReviewMinutes int
|
|
Concurrency int
|
|
RunnerImage string
|
|
AllowUntrustedForks bool
|
|
WebhookMaxBytes int64
|
|
}
|
|
|
|
func Load() (Settings, error) {
|
|
s := Settings{
|
|
GiteaBaseURL: strings.TrimRight(os.Getenv("GITEA_BASE_URL"), "/"), GiteaToken: os.Getenv("GITEA_TOKEN"),
|
|
GiteaBotUsername: os.Getenv("GITEA_BOT_USERNAME"), GiteaBotMentions: os.Getenv("GITEA_BOT_MENTIONS"), GiteaWebhookSecret: os.Getenv("GITEA_WEBHOOK_SECRET"),
|
|
OpenAIAPIKey: os.Getenv("OPENAI_API_KEY"), OpenAIProjectID: os.Getenv("OPENAI_PROJECT_ID"), OpenAIOrgID: os.Getenv("OPENAI_ORG_ID"),
|
|
OpenAIReviewModel: envString("OPENAI_REVIEW_MODEL", "gpt-5.3-codex"), CodexAuthMode: envString("CODEX_AUTH_MODE", "api_key"), CodexAuthJSONPath: envString("CODEX_AUTH_JSON_PATH", "~/.codex/auth.json"),
|
|
AllowedRepos: splitCSV(os.Getenv("ALLOWED_REPOS")), WebhookMode: envString("WEBHOOK_MODE", "repo"), DatabaseURL: os.Getenv("DATABASE_URL"),
|
|
DBHost: os.Getenv("DB_HOST"), DBName: os.Getenv("DB_NAME"), DBUser: os.Getenv("DB_USER"), DBPassword: os.Getenv("DB_PASSWORD"),
|
|
Workdir: envString("WORKDIR", "/var/lib/gitea-codex/worktrees"), RunnerImage: envString("REVIEW_RUNNER_IMAGE", "node:22-bookworm-slim"),
|
|
}
|
|
var err error
|
|
if s.DBPort, err = envInt("DB_PORT", 3306); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
if s.CooldownSeconds, err = envInt("COOLDOWN_SECONDS", 60); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
if s.MaxDiffBytes, err = envInt("MAX_DIFF_BYTES", 200000); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
if s.MaxReviewMinutes, err = envInt("MAX_REVIEW_MINUTES", 10); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
if s.Concurrency, err = envInt("CONCURRENCY", 1); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
maxBytes, err := envInt("WEBHOOK_MAX_BYTES", 2*1024*1024)
|
|
if err != nil {
|
|
return Settings{}, err
|
|
}
|
|
s.WebhookMaxBytes = int64(maxBytes)
|
|
s.AllowUntrustedForks, err = envBool("ALLOW_UNTRUSTED_FORKS", false)
|
|
if err != nil {
|
|
return Settings{}, err
|
|
}
|
|
if err := s.Validate(); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (s Settings) Validate() error {
|
|
for name, value := range map[string]string{"GITEA_BASE_URL": s.GiteaBaseURL, "GITEA_TOKEN": s.GiteaToken, "GITEA_BOT_USERNAME": s.GiteaBotUsername, "GITEA_WEBHOOK_SECRET": s.GiteaWebhookSecret} {
|
|
if strings.TrimSpace(value) == "" {
|
|
return fmt.Errorf("%s is required", name)
|
|
}
|
|
}
|
|
if len(s.AllowedRepos) == 0 {
|
|
return errors.New("ALLOWED_REPOS is required")
|
|
}
|
|
if s.CodexAuthMode != "api_key" && s.CodexAuthMode != "chatgpt" {
|
|
return errors.New("CODEX_AUTH_MODE must be api_key or chatgpt")
|
|
}
|
|
if s.CodexAuthMode == "api_key" && strings.TrimSpace(s.OpenAIAPIKey) == "" {
|
|
return errors.New("OPENAI_API_KEY is required")
|
|
}
|
|
if s.CooldownSeconds < 0 || s.MaxDiffBytes <= 0 || s.MaxReviewMinutes <= 0 || s.Concurrency <= 0 || s.WebhookMaxBytes <= 0 {
|
|
return errors.New("numeric configuration values are invalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s Settings) RepoAllowed(repo string) bool {
|
|
for _, allowed := range s.AllowedRepos {
|
|
if allowed == repo {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func (s Settings) Aliases() map[string]bool {
|
|
out := map[string]bool{"codex": true, strings.ToLower(strings.TrimPrefix(strings.TrimSpace(s.GiteaBotUsername), "@")): true}
|
|
for _, x := range splitCSV(s.GiteaBotMentions) {
|
|
out[strings.ToLower(strings.TrimPrefix(x, "@"))] = true
|
|
}
|
|
return out
|
|
}
|
|
func envString(name, fallback string) string {
|
|
if v := os.Getenv(name); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
func envInt(name string, fallback int) (int, error) {
|
|
v := envString(name, strconv.Itoa(fallback))
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return n, nil
|
|
}
|
|
func envBool(name string, fallback bool) (bool, error) {
|
|
v := os.Getenv(name)
|
|
if v == "" {
|
|
return fallback, nil
|
|
}
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return b, nil
|
|
}
|
|
func splitCSV(value string) []string {
|
|
var out []string
|
|
for _, item := range strings.Split(value, ",") {
|
|
if v := strings.TrimSpace(item); v != "" {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out
|
|
}
|