diff --git a/machines/storage01/forgejo.nix b/machines/storage01/forgejo.nix index ae1750d..08e2541 100644 --- a/machines/storage01/forgejo.nix +++ b/machines/storage01/forgejo.nix @@ -7,7 +7,7 @@ let host = "git.dgnum.eu"; in { - services.gitea = { + services.forgejo = { enable = true; user = "git"; @@ -19,6 +19,7 @@ in { database = { type = "postgres"; user = "git"; + name = "gitea"; passwordFile = config.age.secrets."forgejo-database_password_file".path; }; diff --git a/nix-patches/248310.patch b/nix-patches/248310.patch new file mode 100644 index 0000000..2288602 --- /dev/null +++ b/nix-patches/248310.patch @@ -0,0 +1,914 @@ +From 8d374cebcd8736d19c289e6d1166ab0b7428adc7 Mon Sep 17 00:00:00 2001 +From: emilylange +Date: Sun, 6 Aug 2023 18:40:02 +0200 +Subject: [PATCH 1/3] nixos/forgejo: init + +Following a decicion from both the gitea and forgejo maintainers in +nixpkgs. +This means, that forgejo will no longer co-use the nixos/gitea module +via `services.gitea.package = pkgs.forgejo`. +--- + nixos/modules/module-list.nix | 1 + + nixos/modules/services/misc/forgejo.nix | 668 ++++++++++++++++++++++++ + 2 files changed, 669 insertions(+) + create mode 100644 nixos/modules/services/misc/forgejo.nix + +diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix +index 29fcabaefad51e..6ea859b201e475 100644 +--- a/nixos/modules/module-list.nix ++++ b/nixos/modules/module-list.nix +@@ -640,6 +640,7 @@ + ./services/misc/etesync-dav.nix + ./services/misc/evdevremapkeys.nix + ./services/misc/felix.nix ++ ./services/misc/forgejo.nix + ./services/misc/freeswitch.nix + ./services/misc/fstrim.nix + ./services/misc/gammu-smsd.nix +diff --git a/nixos/modules/services/misc/forgejo.nix b/nixos/modules/services/misc/forgejo.nix +new file mode 100644 +index 00000000000000..f26658b7bcb440 +--- /dev/null ++++ b/nixos/modules/services/misc/forgejo.nix +@@ -0,0 +1,668 @@ ++{ config, lib, options, pkgs, ... }: ++ ++let ++ cfg = config.services.forgejo; ++ opt = options.services.forgejo; ++ format = pkgs.formats.ini { }; ++ ++ exe = lib.getExe cfg.package; ++ ++ pg = config.services.postgresql; ++ useMysql = cfg.database.type == "mysql"; ++ usePostgresql = cfg.database.type == "postgres"; ++ useSqlite = cfg.database.type == "sqlite3"; ++ ++ inherit (lib) ++ literalExpression ++ mdDoc ++ mkChangedOptionModule ++ mkDefault ++ mkEnableOption ++ mkIf ++ mkMerge ++ mkOption ++ mkPackageOptionMD ++ mkRemovedOptionModule ++ mkRenamedOptionModule ++ optionalAttrs ++ optionals ++ optionalString ++ types ++ ; ++in ++{ ++ imports = [ ++ (mkRenamedOptionModule [ "services" "forgejo" "appName" ] [ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ]) ++ (mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ] "services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead") ++ (mkRemovedOptionModule [ "services" "forgejo" "database" "password" ] "services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead") ++ ++ # copied from services.gitea; remove at some point ++ (mkRenamedOptionModule [ "services" "forgejo" "cookieSecure" ] [ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "disableRegistration" ] [ "services" "forgejo" "settings" "service" "DISABLE_REGISTRATION" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "domain" ] [ "services" "forgejo" "settings" "server" "DOMAIN" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "httpAddress" ] [ "services" "forgejo" "settings" "server" "HTTP_ADDR" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "httpPort" ] [ "services" "forgejo" "settings" "server" "HTTP_PORT" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "log" "level" ] [ "services" "forgejo" "settings" "log" "LEVEL" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "log" "rootPath" ] [ "services" "forgejo" "settings" "log" "ROOT_PATH" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "rootUrl" ] [ "services" "forgejo" "settings" "server" "ROOT_URL" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "ssh" "clonePort" ] [ "services" "forgejo" "settings" "server" "SSH_PORT" ]) ++ (mkRenamedOptionModule [ "services" "forgejo" "staticRootPath" ] [ "services" "forgejo" "settings" "server" "STATIC_ROOT_PATH" ]) ++ (mkChangedOptionModule [ "services" "forgejo" "enableUnixSocket" ] [ "services" "forgejo" "settings" "server" "PROTOCOL" ] ( ++ config: if config.services.forgejo.enableUnixSocket then "http+unix" else "http" ++ )) ++ (mkRemovedOptionModule [ "services" "forgejo" "ssh" "enable" ] "services.forgejo.ssh.enable has been migrated into freeform setting services.forgejo.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted") ++ ]; ++ ++ options = { ++ services.forgejo = { ++ enable = mkEnableOption (mdDoc "Forgejo"); ++ ++ package = mkPackageOptionMD pkgs "forgejo" { }; ++ ++ useWizard = mkOption { ++ default = false; ++ type = types.bool; ++ description = mdDoc '' ++ Whether to use the built-in installation wizard instead of ++ declaratively managing the {file}`app.ini` config file in nix. ++ ''; ++ }; ++ ++ stateDir = mkOption { ++ default = "/var/lib/forgejo"; ++ type = types.str; ++ description = mdDoc "Forgejo data directory."; ++ }; ++ ++ customDir = mkOption { ++ default = "${cfg.stateDir}/custom"; ++ defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"''; ++ type = types.str; ++ description = mdDoc '' ++ Base directory for custom templates and other options. ++ ++ If {option}`${opt.useWizard}` is disabled (default), this directory will also ++ hold secrets and the resulting {file}`app.ini` config at runtime. ++ ''; ++ }; ++ ++ user = mkOption { ++ type = types.str; ++ default = "forgejo"; ++ description = mdDoc "User account under which Forgejo runs."; ++ }; ++ ++ group = mkOption { ++ type = types.str; ++ default = "forgejo"; ++ description = mdDoc "Group under which Forgejo runs."; ++ }; ++ ++ database = { ++ type = mkOption { ++ type = types.enum [ "sqlite3" "mysql" "postgres" ]; ++ example = "mysql"; ++ default = "sqlite3"; ++ description = mdDoc "Database engine to use."; ++ }; ++ ++ host = mkOption { ++ type = types.str; ++ default = "127.0.0.1"; ++ description = mdDoc "Database host address."; ++ }; ++ ++ port = mkOption { ++ type = types.port; ++ default = if !usePostgresql then 3306 else pg.port; ++ defaultText = literalExpression '' ++ if config.${opt.database.type} != "postgresql" ++ then 3306 ++ else config.${options.services.postgresql.port} ++ ''; ++ description = mdDoc "Database host port."; ++ }; ++ ++ name = mkOption { ++ type = types.str; ++ default = "forgejo"; ++ description = mdDoc "Database name."; ++ }; ++ ++ user = mkOption { ++ type = types.str; ++ default = "forgejo"; ++ description = mdDoc "Database user."; ++ }; ++ ++ passwordFile = mkOption { ++ type = types.nullOr types.path; ++ default = null; ++ example = "/run/keys/forgejo-dbpassword"; ++ description = mdDoc '' ++ A file containing the password corresponding to ++ {option}`${opt.database.user}`. ++ ''; ++ }; ++ ++ socket = mkOption { ++ type = types.nullOr types.path; ++ default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null; ++ defaultText = literalExpression "null"; ++ example = "/run/mysqld/mysqld.sock"; ++ description = mdDoc "Path to the unix socket file to use for authentication."; ++ }; ++ ++ path = mkOption { ++ type = types.str; ++ default = "${cfg.stateDir}/data/forgejo.db"; ++ defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/forgejo.db"''; ++ description = mdDoc "Path to the sqlite3 database file."; ++ }; ++ ++ createDatabase = mkOption { ++ type = types.bool; ++ default = true; ++ description = mdDoc "Whether to create a local database automatically."; ++ }; ++ }; ++ ++ dump = { ++ enable = mkEnableOption (mdDoc "periodic dumps via the [built-in {command}`dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump)"); ++ ++ interval = mkOption { ++ type = types.str; ++ default = "04:31"; ++ example = "hourly"; ++ description = mdDoc '' ++ Run a Forgejo dump at this interval. Runs by default at 04:31 every day. ++ ++ The format is described in ++ {manpage}`systemd.time(7)`. ++ ''; ++ }; ++ ++ backupDir = mkOption { ++ type = types.str; ++ default = "${cfg.stateDir}/dump"; ++ defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"''; ++ description = mdDoc "Path to the directory where the dump archives will be stored."; ++ }; ++ ++ type = mkOption { ++ type = types.enum [ "zip" "tar" "tar.sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" "tar.zst" ]; ++ default = "zip"; ++ description = mdDoc "Archive format used to store the dump file."; ++ }; ++ ++ file = mkOption { ++ type = types.nullOr types.str; ++ default = null; ++ description = mdDoc "Filename to be used for the dump. If `null` a default name is chosen by forgejo."; ++ example = "forgejo-dump"; ++ }; ++ }; ++ ++ lfs = { ++ enable = mkOption { ++ type = types.bool; ++ default = false; ++ description = mdDoc "Enables git-lfs support."; ++ }; ++ ++ contentDir = mkOption { ++ type = types.str; ++ default = "${cfg.stateDir}/data/lfs"; ++ defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"''; ++ description = mdDoc "Where to store LFS files."; ++ }; ++ }; ++ ++ repositoryRoot = mkOption { ++ type = types.str; ++ default = "${cfg.stateDir}/repositories"; ++ defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"''; ++ description = mdDoc "Path to the git repositories."; ++ }; ++ ++ mailerPasswordFile = mkOption { ++ type = types.nullOr types.str; ++ default = null; ++ example = "/run/keys/forgejo-mailpw"; ++ description = mdDoc "Path to a file containing the SMTP password."; ++ }; ++ ++ settings = mkOption { ++ default = { }; ++ description = mdDoc '' ++ Free-form settings written directly to the `app.ini` configfile file. ++ Refer to for supported values. ++ ''; ++ example = literalExpression '' ++ { ++ DEFAULT = { ++ RUN_MODE = "dev"; ++ }; ++ "cron.sync_external_users" = { ++ RUN_AT_START = true; ++ SCHEDULE = "@every 24h"; ++ UPDATE_EXISTING = true; ++ }; ++ mailer = { ++ ENABLED = true; ++ MAILER_TYPE = "sendmail"; ++ FROM = "do-not-reply@example.org"; ++ SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail"; ++ }; ++ other = { ++ SHOW_FOOTER_VERSION = false; ++ }; ++ } ++ ''; ++ type = types.submodule { ++ freeformType = format.type; ++ options = { ++ log = { ++ ROOT_PATH = mkOption { ++ default = "${cfg.stateDir}/log"; ++ defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"''; ++ type = types.str; ++ description = mdDoc "Root path for log files."; ++ }; ++ LEVEL = mkOption { ++ default = "Info"; ++ type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ]; ++ description = mdDoc "General log level."; ++ }; ++ }; ++ ++ server = { ++ PROTOCOL = mkOption { ++ type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ]; ++ default = "http"; ++ description = mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."''; ++ }; ++ ++ HTTP_ADDR = mkOption { ++ type = types.either types.str types.path; ++ default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"; ++ defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"''; ++ description = mdDoc "Listen address. Must be a path when using a unix socket."; ++ }; ++ ++ HTTP_PORT = mkOption { ++ type = types.port; ++ default = 3000; ++ description = mdDoc "Listen port. Ignored when using a unix socket."; ++ }; ++ ++ DOMAIN = mkOption { ++ type = types.str; ++ default = "localhost"; ++ description = mdDoc "Domain name of your server."; ++ }; ++ ++ ROOT_URL = mkOption { ++ type = types.str; ++ default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/"; ++ defaultText = literalExpression ''"http://''${config.services.forgejo.settings.server.DOMAIN}:''${toString config.services.forgejo.settings.server.HTTP_PORT}/"''; ++ description = mdDoc "Full public URL of Forgejo server."; ++ }; ++ ++ STATIC_ROOT_PATH = mkOption { ++ type = types.either types.str types.path; ++ default = cfg.package.data; ++ defaultText = literalExpression "config.${opt.package}.data"; ++ example = "/var/lib/forgejo/data"; ++ description = mdDoc "Upper level of template and static files path."; ++ }; ++ ++ DISABLE_SSH = mkOption { ++ type = types.bool; ++ default = false; ++ description = mdDoc "Disable external SSH feature."; ++ }; ++ ++ SSH_PORT = mkOption { ++ type = types.port; ++ default = 22; ++ example = 2222; ++ description = mdDoc '' ++ SSH port displayed in clone URL. ++ The option is required to configure a service when the external visible port ++ differs from the local listening port i.e. if port forwarding is used. ++ ''; ++ }; ++ }; ++ ++ session = { ++ COOKIE_SECURE = mkOption { ++ type = types.bool; ++ default = false; ++ description = mdDoc '' ++ Marks session cookies as "secure" as a hint for browsers to only send ++ them via HTTPS. This option is recommend, if Forgejo is being served over HTTPS. ++ ''; ++ }; ++ }; ++ }; ++ }; ++ }; ++ }; ++ }; ++ ++ config = mkIf cfg.enable { ++ assertions = [ ++ { ++ assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user; ++ message = "services.forgejo.database.user must match services.forgejo.user if the database is to be automatically provisioned"; ++ } ++ ]; ++ ++ services.forgejo.settings = { ++ DEFAULT = { ++ RUN_MODE = mkDefault "prod"; ++ RUN_USER = mkDefault cfg.user; ++ WORK_PATH = mkDefault cfg.stateDir; ++ }; ++ ++ database = mkMerge [ ++ { ++ DB_TYPE = cfg.database.type; ++ } ++ (mkIf (useMysql || usePostgresql) { ++ HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port; ++ NAME = cfg.database.name; ++ USER = cfg.database.user; ++ PASSWD = "#dbpass#"; ++ }) ++ (mkIf useSqlite { ++ PATH = cfg.database.path; ++ }) ++ (mkIf usePostgresql { ++ SSL_MODE = "disable"; ++ }) ++ ]; ++ ++ repository = { ++ ROOT = cfg.repositoryRoot; ++ }; ++ ++ server = mkIf cfg.lfs.enable { ++ LFS_START_SERVER = true; ++ LFS_JWT_SECRET = "#lfsjwtsecret#"; ++ }; ++ ++ session = { ++ COOKIE_NAME = mkDefault "session"; ++ }; ++ ++ security = { ++ SECRET_KEY = "#secretkey#"; ++ INTERNAL_TOKEN = "#internaltoken#"; ++ INSTALL_LOCK = true; ++ }; ++ ++ mailer = mkIf (cfg.mailerPasswordFile != null) { ++ PASSWD = "#mailerpass#"; ++ }; ++ ++ oauth2 = { ++ JWT_SECRET = "#oauth2jwtsecret#"; ++ }; ++ ++ lfs = mkIf cfg.lfs.enable { ++ PATH = cfg.lfs.contentDir; ++ }; ++ }; ++ ++ services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { ++ enable = mkDefault true; ++ ++ ensureDatabases = [ cfg.database.name ]; ++ ensureUsers = [ ++ { ++ name = cfg.database.user; ++ ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; ++ } ++ ]; ++ }; ++ ++ services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) { ++ enable = mkDefault true; ++ package = mkDefault pkgs.mariadb; ++ ++ ensureDatabases = [ cfg.database.name ]; ++ ensureUsers = [ ++ { ++ name = cfg.database.user; ++ ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; ++ } ++ ]; ++ }; ++ ++ systemd.tmpfiles.rules = [ ++ "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" ++ "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" ++ ++ # If we have a folder or symlink with Forgejo locales, remove it ++ # And symlink the current Forgejo locales in place ++ "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" ++ ++ ] ++ optionals cfg.lfs.enable [ ++ "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" ++ ]; ++ ++ systemd.services.forgejo = { ++ description = "Forgejo (Beyond coding. We forge.)"; ++ after = [ ++ "network.target" ++ ] ++ optionals usePostgresql [ ++ "postgresql.service" ++ ] ++ optionals useMysql [ ++ "mysql.service" ++ ]; ++ requires = optionals (cfg.database.createDatabase && usePostgresql) [ ++ "postgresql.service" ++ ] ++ optionals (cfg.database.createDatabase && useMysql) [ ++ "mysql.service" ++ ]; ++ wantedBy = [ "multi-user.target" ]; ++ path = [ cfg.package pkgs.git pkgs.gnupg ]; ++ ++ # In older versions the secret naming for JWT was kind of confusing. ++ # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET ++ # wasn't persistent at all. ++ # To fix that, there is now the file oauth2_jwt_secret containing the ++ # values for JWT_SECRET and the file jwt_secret gets renamed to ++ # lfs_jwt_secret. ++ # We have to consider this to stay compatible with older installations. ++ preStart = ++ let ++ runConfig = "${cfg.customDir}/conf/app.ini"; ++ secretKey = "${cfg.customDir}/conf/secret_key"; ++ oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret"; ++ oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET ++ lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET ++ internalToken = "${cfg.customDir}/conf/internal_token"; ++ replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; ++ in ++ '' ++ # copy custom configuration and generate random secrets if needed ++ ${lib.optionalString (!cfg.useWizard) '' ++ function forgejo_setup { ++ cp -f '${format.generate "app.ini" cfg.settings}' '${runConfig}' ++ ++ if [ ! -s '${secretKey}' ]; then ++ ${exe} generate secret SECRET_KEY > '${secretKey}' ++ fi ++ ++ # Migrate LFS_JWT_SECRET filename ++ if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then ++ mv '${oldLfsJwtSecret}' '${lfsJwtSecret}' ++ fi ++ ++ if [ ! -s '${oauth2JwtSecret}' ]; then ++ ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}' ++ fi ++ ++ ${optionalString cfg.lfs.enable '' ++ if [ ! -s '${lfsJwtSecret}' ]; then ++ ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}' ++ fi ++ ''} ++ ++ if [ ! -s '${internalToken}' ]; then ++ ${exe} generate secret INTERNAL_TOKEN > '${internalToken}' ++ fi ++ ++ chmod u+w '${runConfig}' ++ ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' ++ ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' ++ ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' ++ ++ ${optionalString cfg.lfs.enable '' ++ ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' ++ ''} ++ ++ ${optionalString (cfg.database.passwordFile != null) '' ++ ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' ++ ''} ++ ++ ${optionalString (cfg.mailerPasswordFile != null) '' ++ ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' ++ ''} ++ chmod u-w '${runConfig}' ++ } ++ (umask 027; forgejo_setup) ++ ''} ++ ++ # run migrations/init the database ++ ${exe} migrate ++ ++ # update all hooks' binary paths ++ ${exe} admin regenerate hooks ++ ++ # update command option in authorized_keys ++ if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] ++ then ++ ${exe} admin regenerate keys ++ fi ++ ''; ++ ++ serviceConfig = { ++ Type = "simple"; ++ User = cfg.user; ++ Group = cfg.group; ++ WorkingDirectory = cfg.stateDir; ++ ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid"; ++ Restart = "always"; ++ # Runtime directory and mode ++ RuntimeDirectory = "forgejo"; ++ RuntimeDirectoryMode = "0755"; ++ # Proc filesystem ++ ProcSubset = "pid"; ++ ProtectProc = "invisible"; ++ # Access write directories ++ ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; ++ UMask = "0027"; ++ # Capabilities ++ CapabilityBoundingSet = ""; ++ # Security ++ NoNewPrivileges = true; ++ # Sandboxing ++ ProtectSystem = "strict"; ++ ProtectHome = true; ++ PrivateTmp = true; ++ PrivateDevices = true; ++ PrivateUsers = true; ++ ProtectHostname = true; ++ ProtectClock = true; ++ ProtectKernelTunables = true; ++ ProtectKernelModules = true; ++ ProtectKernelLogs = true; ++ ProtectControlGroups = true; ++ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; ++ RestrictNamespaces = true; ++ LockPersonality = true; ++ MemoryDenyWriteExecute = true; ++ RestrictRealtime = true; ++ RestrictSUIDSGID = true; ++ RemoveIPC = true; ++ PrivateMounts = true; ++ # System Call Filtering ++ SystemCallArchitectures = "native"; ++ SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" "setrlimit" ]; ++ }; ++ ++ environment = { ++ USER = cfg.user; ++ HOME = cfg.stateDir; ++ # `GITEA_` prefix until https://codeberg.org/forgejo/forgejo/issues/497 ++ # is resolved. ++ GITEA_WORK_DIR = cfg.stateDir; ++ GITEA_CUSTOM = cfg.customDir; ++ }; ++ }; ++ ++ users.users = mkIf (cfg.user == "forgejo") { ++ forgejo = { ++ home = cfg.stateDir; ++ useDefaultShell = true; ++ group = cfg.group; ++ isSystemUser = true; ++ }; ++ }; ++ ++ users.groups = mkIf (cfg.group == "forgejo") { ++ forgejo = { }; ++ }; ++ ++ systemd.services.forgejo-dump = mkIf cfg.dump.enable { ++ description = "forgejo dump"; ++ after = [ "forgejo.service" ]; ++ path = [ cfg.package ]; ++ ++ environment = { ++ USER = cfg.user; ++ HOME = cfg.stateDir; ++ # `GITEA_` prefix until https://codeberg.org/forgejo/forgejo/issues/497 ++ # is resolved. ++ GITEA_WORK_DIR = cfg.stateDir; ++ GITEA_CUSTOM = cfg.customDir; ++ }; ++ ++ serviceConfig = { ++ Type = "oneshot"; ++ User = cfg.user; ++ ExecStart = "${exe} dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}"; ++ WorkingDirectory = cfg.dump.backupDir; ++ }; ++ }; ++ ++ systemd.timers.forgejo-dump = mkIf cfg.dump.enable { ++ description = "Forgejo dump timer"; ++ partOf = [ "forgejo-dump.service" ]; ++ wantedBy = [ "timers.target" ]; ++ timerConfig.OnCalendar = cfg.dump.interval; ++ }; ++ }; ++ ++ meta.maintainers = with lib.maintainers; [ bendlas emilylange ]; ++} + +From 02601e17a53eadd488bd8ca16dbb656fd46d1764 Mon Sep 17 00:00:00 2001 +From: emilylange +Date: Sun, 6 Aug 2023 18:41:37 +0200 +Subject: [PATCH 2/3] nixosTests.forgejo: fork from nixosTests.gitea + +--- + nixos/tests/all-tests.nix | 2 +- + nixos/tests/forgejo.nix | 157 ++++++++++++++++++++++++++++++++++++++ + 2 files changed, 158 insertions(+), 1 deletion(-) + create mode 100644 nixos/tests/forgejo.nix + +diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix +index 3b4a39f5ff96b8..d9aa9eccac02d2 100644 +--- a/nixos/tests/all-tests.nix ++++ b/nixos/tests/all-tests.nix +@@ -280,7 +280,7 @@ in { + fluentd = handleTest ./fluentd.nix {}; + fluidd = handleTest ./fluidd.nix {}; + fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {}; +- forgejo = handleTest ./gitea.nix { giteaPackage = pkgs.forgejo; }; ++ forgejo = handleTest ./forgejo.nix { }; + freenet = handleTest ./freenet.nix {}; + freeswitch = handleTest ./freeswitch.nix {}; + freshrss-sqlite = handleTest ./freshrss-sqlite.nix {}; +diff --git a/nixos/tests/forgejo.nix b/nixos/tests/forgejo.nix +new file mode 100644 +index 00000000000000..b326819e319064 +--- /dev/null ++++ b/nixos/tests/forgejo.nix +@@ -0,0 +1,157 @@ ++{ system ? builtins.currentSystem ++, config ? { } ++, pkgs ? import ../.. { inherit system config; } ++}: ++ ++with import ../lib/testing-python.nix { inherit system pkgs; }; ++with pkgs.lib; ++ ++let ++ ## gpg --faked-system-time='20230301T010000!' --quick-generate-key snakeoil ed25519 sign ++ signingPrivateKey = '' ++ -----BEGIN PGP PRIVATE KEY BLOCK----- ++ ++ lFgEY/6jkBYJKwYBBAHaRw8BAQdADXiZRV8RJUyC9g0LH04wLMaJL9WTc+szbMi7 ++ 5fw4yP8AAQCl8EwGfzSLm/P6fCBfA3I9znFb3MEHGCCJhJ6VtKYyRw7ktAhzbmFr ++ ZW9pbIiUBBMWCgA8FiEE+wUM6VW/NLtAdSixTWQt6LZ4x50FAmP+o5ACGwMFCQPC ++ ZwAECwkIBwQVCgkIBRYCAwEAAh4FAheAAAoJEE1kLei2eMedFTgBAKQs1oGFZrCI ++ TZP42hmBTKxGAI1wg7VSdDEWTZxut/2JAQDGgo2sa4VHMfj0aqYGxrIwfP2B7JHO ++ GCqGCRf9O/hzBA== ++ =9Uy3 ++ -----END PGP PRIVATE KEY BLOCK----- ++ ''; ++ signingPrivateKeyId = "4D642DE8B678C79D"; ++ ++ supportedDbTypes = [ "mysql" "postgres" "sqlite3" ]; ++ makeGForgejoTest = type: nameValuePair type (makeTest { ++ name = "forgejo-${type}"; ++ meta.maintainers = with maintainers; [ bendlas emilylange ]; ++ ++ nodes = { ++ server = { config, pkgs, ... }: { ++ virtualisation.memorySize = 2047; ++ services.forgejo = { ++ enable = true; ++ database = { inherit type; }; ++ settings.service.DISABLE_REGISTRATION = true; ++ settings."repository.signing".SIGNING_KEY = signingPrivateKeyId; ++ settings.actions.ENABLED = true; ++ }; ++ environment.systemPackages = [ config.services.forgejo.package pkgs.gnupg pkgs.jq ]; ++ services.openssh.enable = true; ++ ++ specialisation.runner = { ++ inheritParentConfig = true; ++ configuration.services.gitea-actions-runner.instances."test" = { ++ enable = true; ++ name = "ci"; ++ url = "http://localhost:3000"; ++ labels = [ ++ # don't require docker/podman ++ "native:host" ++ ]; ++ tokenFile = "/var/lib/forgejo/runner_token"; ++ }; ++ }; ++ }; ++ client1 = { config, pkgs, ... }: { ++ environment.systemPackages = [ pkgs.git ]; ++ }; ++ client2 = { config, pkgs, ... }: { ++ environment.systemPackages = [ pkgs.git ]; ++ }; ++ }; ++ ++ testScript = { nodes, ... }: ++ let ++ inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; ++ serverSystem = nodes.server.system.build.toplevel; ++ in ++ '' ++ GIT_SSH_COMMAND = "ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no" ++ REPO = "forgejo@server:test/repo" ++ PRIVK = "${snakeOilPrivateKey}" ++ ++ start_all() ++ ++ client1.succeed("mkdir /tmp/repo") ++ client1.succeed("mkdir -p $HOME/.ssh") ++ client1.succeed(f"cat {PRIVK} > $HOME/.ssh/privk") ++ client1.succeed("chmod 0400 $HOME/.ssh/privk") ++ client1.succeed("git -C /tmp/repo init") ++ client1.succeed("echo hello world > /tmp/repo/testfile") ++ client1.succeed("git -C /tmp/repo add .") ++ client1.succeed("git config --global user.email test@localhost") ++ client1.succeed("git config --global user.name test") ++ client1.succeed("git -C /tmp/repo commit -m 'Initial import'") ++ client1.succeed(f"git -C /tmp/repo remote add origin {REPO}") ++ ++ server.wait_for_unit("forgejo.service") ++ server.wait_for_open_port(3000) ++ server.wait_for_open_port(22) ++ server.succeed("curl --fail http://localhost:3000/") ++ ++ server.succeed( ++ "su -l forgejo -c 'gpg --homedir /var/lib/forgejo/data/home/.gnupg " ++ + "--import ${toString (pkgs.writeText "forgejo.key" signingPrivateKey)}'" ++ ) ++ ++ assert "BEGIN PGP PUBLIC KEY BLOCK" in server.succeed("curl http://localhost:3000/api/v1/signing-key.gpg") ++ ++ server.succeed( ++ "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. " ++ + "Please contact your site administrator.'" ++ ) ++ server.succeed( ++ "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea admin user create " ++ + "--username test --password totallysafe --email test@localhost'" ++ ) ++ ++ api_token = server.succeed( ++ "curl --fail -X POST http://test:totallysafe@localhost:3000/api/v1/users/test/tokens " ++ + "-H 'Accept: application/json' -H 'Content-Type: application/json' -d " ++ + "'{\"name\":\"token\",\"scopes\":[\"all\"]}' | jq '.sha1' | xargs echo -n" ++ ) ++ ++ server.succeed( ++ "curl --fail -X POST http://localhost:3000/api/v1/user/repos " ++ + "-H 'Accept: application/json' -H 'Content-Type: application/json' " ++ + f"-H 'Authorization: token {api_token}'" ++ + ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\''' ++ ) ++ ++ server.succeed( ++ "curl --fail -X POST http://localhost:3000/api/v1/user/keys " ++ + "-H 'Accept: application/json' -H 'Content-Type: application/json' " ++ + f"-H 'Authorization: token {api_token}'" ++ + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\''' ++ ) ++ ++ client1.succeed( ++ f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git -C /tmp/repo push origin master" ++ ) ++ ++ client2.succeed("mkdir -p $HOME/.ssh") ++ client2.succeed(f"cat {PRIVK} > $HOME/.ssh/privk") ++ client2.succeed("chmod 0400 $HOME/.ssh/privk") ++ client2.succeed(f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git clone {REPO}") ++ client2.succeed('test "$(cat repo/testfile | xargs echo -n)" = "hello world"') ++ ++ server.wait_until_succeeds( ++ 'test "$(curl http://localhost:3000/api/v1/repos/test/repo/commits ' ++ + '-H "Accept: application/json" | jq length)" = "1"', ++ timeout=10 ++ ) ++ ++ with subtest("Testing runner registration"): ++ server.succeed( ++ "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token" ++ ) ++ server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test") ++ server.wait_for_unit("gitea-runner-test.service") ++ server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'") ++ ''; ++ }); ++in ++ ++listToAttrs (map makeGForgejoTest supportedDbTypes) + +From 7b786b39cb0d42949720482b78c31fcfe35b41c7 Mon Sep 17 00:00:00 2001 +From: emilylange +Date: Sun, 6 Aug 2023 18:43:08 +0200 +Subject: [PATCH 3/3] CODEOWNERS: init forgejo + +--- + .github/CODEOWNERS | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS +index 98a7022088eb22..829ce356f9db37 100644 +--- a/.github/CODEOWNERS ++++ b/.github/CODEOWNERS +@@ -288,6 +288,10 @@ pkgs/development/python-modules/buildcatrust/ @ajs124 @lukegb @mweinelt + /nixos/modules/services/misc/matrix-conduit.nix @piegamesde + /nixos/tests/matrix-conduit.nix @piegamesde + ++# Forgejo ++nixos/modules/services/misc/forgejo.nix @bendlas @emilylange ++pkgs/applications/version-management/forgejo @bendlas @emilylange ++ + # Dotnet + /pkgs/build-support/dotnet @IvarWithoutBones + /pkgs/development/compilers/dotnet @IvarWithoutBones diff --git a/nix-patches/default.nix b/nix-patches/default.nix index 9f3e0da..05d3fe4 100644 --- a/nix-patches/default.nix +++ b/nix-patches/default.nix @@ -8,5 +8,8 @@ # Forgejo 1.20.4-0 ./forgejo.patch + + # Forgejo module + ./248310.patch ]; }