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,151 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user