// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package config is the complete, environment-driven runtime configuration of // the license bot. The binary takes no arguments: the container image carries // no secrets and the deployment decides everything through the environment. package config import ( "fmt" "os" "strings" ) // Config is the complete runtime configuration of the bot. type Config struct { // GiteaURL is the instance base URL the bot calls, e.g. http://gitea:3000 // on an instance whose Gitea shares this Docker daemon. Deliberately // configured here instead of taken from webhook payloads: the payload's // clone_url follows the instance ROOT_URL, which may be stale. GiteaURL string // PublicURL is the instance URL a *human* should follow, used only for the // links in the bot's PR comments. On an instance reached over the internal // alias (GITEA_URL=http://gitea:3000) that alias is not resolvable from a // browser, so the comment must link the public name instead. Defaults to // GiteaURL. PublicURL string // Token is a bot-account token with write access to the target repositories. // It lives only in this process's environment -- never in a workflow. Token string // WebhookSecret verifies X-Gitea-Signature on incoming webhook deliveries. // Webhooks are the only trigger, so this is required. WebhookSecret string // Repos is the allowlist of owner/name repositories the bot may touch. Repos []string // Listen is the HTTP listen address (webhook + health route). Listen string // BotName/BotEmail identify the licensing commit's committer. BotName string BotEmail string // Holder is passed to addlicense -c. Holder string // AddlicenseBin is the addlicense executable (in PATH by default). AddlicenseBin string // WorkRoot is where per-PR git worktrees are created. WorkRoot string // DryRun inspects and comments but never commits or pushes. DryRun bool // CommentMarker identifies the bot's own PR comments so they can be // replaced (deleted and reposted, never edited in place: Gitea keeps a // comment's position, so an edited message stays buried in the timeline). CommentMarker string // StatusContext is the commit-status context the bot posts and the merge // gate requires. It must match the branch-protection string byte for byte. StatusContext string } // FromEnv builds the configuration from the process environment. func FromEnv() (Config, error) { c := Config{ GiteaURL: strings.TrimRight(env("GITEA_URL", ""), "/"), PublicURL: strings.TrimRight(env("GITEA_PUBLIC_URL", ""), "/"), Token: os.Getenv("GITEA_TOKEN"), WebhookSecret: os.Getenv("WEBHOOK_SECRET"), Repos: splitList(os.Getenv("REPOS")), Listen: env("LISTEN", ":8080"), BotName: env("BOT_NAME", "Dcntrlized Auto CopyLeft Bot"), BotEmail: env("BOT_EMAIL", "copyleft+bot@dcntrlized.org"), Holder: env("HOLDER", "Dcntrlized"), AddlicenseBin: env("ADDLICENSE_BIN", "addlicense"), WorkRoot: env("WORK_ROOT", os.TempDir()), DryRun: truthy(os.Getenv("DRY_RUN")), CommentMarker: env("COMMENT_MARKER", ""), StatusContext: env("STATUS_CONTEXT", "license-bot / addlicense"), } if c.Token == "" { return c, fmt.Errorf("GITEA_TOKEN is required") } if len(c.Repos) == 0 { return c, fmt.Errorf("REPOS is required (comma-separated owner/name list)") } if c.WebhookSecret == "" { return c, fmt.Errorf("WEBHOOK_SECRET is required: webhooks are the only trigger") } if c.PublicURL == "" { c.PublicURL = c.GiteaURL } return c, nil } // Allows reports whether repo (owner/name) is on the allowlist. func (c Config) Allows(repo string) bool { for _, r := range c.Repos { if strings.EqualFold(r, repo) { return true } } return false } func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func truthy(v string) bool { return v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes") } func splitList(v string) []string { var out []string for _, p := range strings.Split(v, ",") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out }