This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
gitea-codex/internal/config/config.go
T
Space-Banane f19b271642
ci / test (pull_request) Successful in 1m54s
ci / publish (pull_request) Has been skipped
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>
2026-07-12 21:51:01 +02:00

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
}