{ config, lib, pkgs, ... }: let cfg = config.services.renovate-bot; inherit (lib) mkOption mkEnableOption types ; renovateConfig = cfg.settings // { platform = cfg.platform; endpoint = cfg.endpoint; }; renovateConfigFile = pkgs.writeText "renovate-config.json" (builtins.toJSON renovateConfig); renovateWrapper = pkgs.writeShellScript "renovate-wrapper" '' export RENOVATE_TOKEN=$(cat "${cfg.tokenFile}") exec ${lib.getExe cfg.package} ${lib.concatStringsSep " " cfg.repositories} ''; in { options = { services.renovate-bot = { enable = mkEnableOption "Renovate Bot service"; package = mkOption { type = types.package; default = pkgs.renovate; defaultText = "pkgs.renovate"; description = "Renovate Bot package to use"; }; platform = mkOption { type = types.enum [ "gitlab" "github" "bitbucket" "azure" ]; default = "gitlab"; description = "Git platform to use"; }; endpoint = mkOption { type = types.str; default = "https://gitlab.com"; description = "Git platform endpoint URL"; example = "https://gitlab.example.com"; }; tokenFile = mkOption { type = types.str; description = "Path to file containing authentication token"; }; envFile = mkOption { type = types.nullOr types.str; default = null; description = "Path to file containing environment variables (in .env format)"; }; user = mkOption { type = types.str; default = "renovate-bot"; description = "User account under which renovate-bot runs"; }; group = mkOption { type = types.str; default = "renovate-bot"; description = "Group account under which renovate-bot runs"; }; homeDirectory = mkOption { type = types.str; default = "/var/lib/renovate-bot"; description = "Home directory for the renovate-bot user"; }; repositories = mkOption { type = types.listOf types.str; default = [ ]; description = "List of repositories to monitor (format: owner/repo)"; example = [ "myorg/myrepo" "myorg/another-repo" ]; }; schedule = mkOption { type = types.nullOr types.str; default = null; example = "before 6am"; description = "Schedule for Renovate runs (systemd timer OnCalendar format)"; }; logLevel = mkOption { type = types.enum [ "fatal" "error" "warn" "info" "debug" "trace" ]; default = "info"; description = "Log level for Renovate (set via LOG_LEVEL environment variable)"; }; extraPackages = mkOption { type = types.listOf types.package; default = [ ]; description = "Extra packages to add to PATH for the Renovate Bot service"; example = "[ pkgs.curl pkgs.jq ]"; }; nodeMemoryLimit = mkOption { type = types.int; default = 4096; description = "Node.js memory limit in MB (--max-old-space-size)"; }; settings = mkOption { default = { }; type = types.submodule { freeformType = with types; attrsOf (oneOf [ str bool int float (listOf str) (attrsOf anything) ]); options = { # Common Renovate configuration options # Based on https://docs.renovatebot.com/configuration-options/ requireConfig = mkOption { type = types.bool; default = false; description = "Require renovate.json config file"; }; dryRun = mkOption { type = types.bool; default = false; description = "Run in dry-run mode (no PRs created)"; }; printConfig = mkOption { type = types.bool; default = false; description = "Print the resolved config and exit"; }; gitAuthor = mkOption { type = types.nullOr types.str; default = null; example = "Renovate Bot "; description = "Git author for commits"; }; gitPrivateKey = mkOption { type = types.nullOr types.str; default = null; description = "Private key for git operations"; }; includeForks = mkOption { type = types.bool; default = false; description = "Include forked repositories"; }; includeMirrors = mkOption { type = types.bool; default = false; description = "Include mirrored repositories"; }; autodiscover = mkOption { type = types.bool; default = false; description = "Auto-discover repositories"; }; autodiscoverFilter = mkOption { type = types.nullOr types.str; default = null; description = "Filter for auto-discovered repositories"; }; timezone = mkOption { type = types.nullOr types.str; default = null; example = "America/Sao_Paulo"; description = "Timezone for scheduling"; }; prConcurrentLimit = mkOption { type = types.int; default = 10; description = "Maximum number of concurrent PRs"; }; prHourlyLimit = mkOption { type = types.int; default = 2; description = "Maximum number of PRs per hour (0 = unlimited)"; }; branchConcurrentLimit = mkOption { type = types.int; default = 10; description = "Maximum number of concurrent branches (0 = unlimited)"; }; }; }; description = '' Settings for the Renovate configuration. See https://docs.renovatebot.com/configuration-options/ for detailed information about all available settings. ''; }; }; }; config = lib.mkIf cfg.enable { warnings = ( lib.optional (lib.isStorePath cfg.tokenFile) '' services.renovate-bot.tokenFile points to a file in the Nix store. You should use a quoted absolute path instead. '' ); users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; home = cfg.homeDirectory; createHome = true; }; users.groups.${cfg.group} = { }; systemd.services.renovate-bot = { description = "Renovate Bot Service"; after = [ "network.target" ]; path = [ pkgs.git ] ++ cfg.extraPackages; environment = { RENOVATE_CONFIG_FILE = renovateConfigFile; LOG_LEVEL = cfg.logLevel; NODE_OPTIONS = "--max-old-space-size=${toString cfg.nodeMemoryLimit}"; }; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; WorkingDirectory = cfg.homeDirectory; ExecStart = "${renovateWrapper}"; } // lib.optionalAttrs (cfg.envFile != null) { EnvironmentFile = cfg.envFile; }; }; systemd.timers.renovate-bot = lib.mkIf (cfg.schedule != null) { description = "Renovate Bot Timer"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = cfg.schedule; Persistent = true; }; }; }; }