feat(forgejo): Switch to forgejo module
This commit is contained in:
parent
7395f240ee
commit
be8ac1a424
3 changed files with 919 additions and 1 deletions
|
@ -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
914
nix-patches/248310.patch
Normal 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
|
|
@ -8,5 +8,8 @@
|
|||
|
||||
# Forgejo 1.20.4-0
|
||||
./forgejo.patch
|
||||
|
||||
# Forgejo module
|
||||
./248310.patch
|
||||
];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue