feat(forgejo): Switch to forgejo module

This commit is contained in:
Tom Hubrecht 2023-09-18 20:55:16 +02:00
parent 7395f240ee
commit be8ac1a424
3 changed files with 919 additions and 1 deletions

View file

@ -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;
};

914
nix-patches/248310.patch Normal file
View file

@ -0,0 +1,914 @@
From 8d374cebcd8736d19c289e6d1166ab0b7428adc7 Mon Sep 17 00:00:00 2001
From: emilylange <git@emilylange.de>
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 <https://forgejo.org/docs/latest/admin/config-cheat-sheet/> 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 <git@emilylange.de>
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 <git@emilylange.de>
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

View file

@ -8,5 +8,8 @@
# Forgejo 1.20.4-0
./forgejo.patch
# Forgejo module
./248310.patch
];
}