about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md14
-rw-r--r--nixos/modules/config/zram.nix2
-rw-r--r--nixos/modules/misc/nixpkgs.nix7
-rw-r--r--nixos/modules/module-list.nix4
-rw-r--r--nixos/modules/programs/soundmodem.nix34
-rw-r--r--nixos/modules/services/continuous-integration/gitea-actions-runner.nix2
-rw-r--r--nixos/modules/services/misc/db-rest.nix182
-rw-r--r--nixos/modules/services/misc/ollama.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/redis.nix1
-rw-r--r--nixos/modules/services/networking/shadowsocks.nix14
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix11
-rw-r--r--nixos/modules/services/x11/urserver.nix2
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/db-rest.nix107
-rw-r--r--nixos/tests/forgejo.nix156
-rw-r--r--nixos/tests/teleport.nix2
17 files changed, 488 insertions, 55 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index ce874a6e0b2d6..f5d1d3016a787 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -53,7 +53,7 @@ In addition to numerous new and updated packages, this release has the following
 
 - [alertmanager-irc-relay](https://github.com/google/alertmanager-irc-relay), a Prometheus Alertmanager IRC Relay. Available as [services.prometheus.alertmanagerIrcRelay](options.html#opt-services.prometheus.alertmanagerIrcRelay.enable).
 
-- [alice-lg](github.com/alice-lg/alice-lg), a looking-glass for BGP sessions. Available as [services.alice-lg](#opt-services.alice-lg.enable).
+- [alice-lg](https://github.com/alice-lg/alice-lg), a looking-glass for BGP sessions. Available as [services.alice-lg](#opt-services.alice-lg.enable).
 
 - [atuin](https://github.com/ellie/atuin), a sync server for shell history. Available as [services.atuin](#opt-services.atuin.enable).
 
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index 988632fc44349..58ceb99b9d7b5 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -37,6 +37,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` for PipeWire and
 `services.pipewire.wireplumber.configPackages` for WirePlumber instead."
 
+- `teleport` has been upgraded from major version 14 to major version 15.
+  Refer to upstream [upgrade instructions](https://goteleport.com/docs/management/operations/upgrading/)
+  and release notes for [v15](https://goteleport.com/docs/changelog/#1500-013124).
+
 - A new option `systemd.sysusers.enable` was added. If enabled, users and
   groups are created with systemd-sysusers instead of with a custom perl script.
 
@@ -118,6 +122,8 @@ Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` for Pi
   Matter Controller Server exposing websocket connections for use with other services, notably Home Assistant.
   Available as [services.matter-server](#opt-services.matter-server.enable)
 
+- [db-rest](https://github.com/derhuerst/db-rest), a wrapper around Deutsche Bahn's internal API for public transport data. Available as [services.db-rest](#opt-services.db-rest.enable).
+
 - [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
 The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been marked deprecated and will be dropped after 24.05 due to lack of maintenance of the anki-sync-server softwares.
 
@@ -417,6 +423,14 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
   - `nomad_1_4` has been removed, as it is now unsupported upstream.
 
+- Dwarf Fortress has been updated to version 50, and its derivations continue to menace with spikes of Nix and bash. Version 50 is identical to the version on Steam, but without the paid elements like tilepacks.
+  dfhack and Dwarf Therapist still work, and older versions are still packaged in case you'd like to roll back. Note that DF 50 saves will not be compatible with DF 0.47 and earlier.
+  See [Bay 12 Games](http://www.bay12games.com/dwarves/) for more details on what's new in Dwarf Fortress.
+
+  - Running an earlier version can be achieved through an override: `dwarf-fortress-packages.dwarf-fortress-full.override { dfVersion = "0.47.5"; }`
+
+  - Ruby plugin support has been disabled in DFHack. Many of the Ruby plugins have been converted to Lua, and support was removed upstream due to frequent crashes.
+
 - The `livebook` package is now built as a `mix release` instead of an `escript`.
   This means that configuration now has to be done using [environment variables](https://hexdocs.pm/livebook/readme.html#environment-variables) instead of command line arguments.
   This has the further implication that the `livebook` service configuration has changed:
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 562485fcc823d..1846ac51eea6e 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -73,7 +73,7 @@ in
       algorithm = lib.mkOption {
         default = "zstd";
         example = "lz4";
-        type = with lib.types; either (enum [ "lzo" "lz4" "zstd" ]) str;
+        type = with lib.types; either (enum [ "842" "lzo" "lzo-rle" "lz4" "lz4hc" "zstd" ]) str;
         description = ''
           Compression algorithm. `lzo` has good compression,
           but is slow. `lz4` has bad compression, but is fast.
diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix
index 433bcd93213d9..60a6fb57c7a29 100644
--- a/nixos/modules/misc/nixpkgs.nix
+++ b/nixos/modules/misc/nixpkgs.nix
@@ -153,11 +153,10 @@ in
         '';
       type = configType;
       description = ''
-        The configuration of the Nix Packages collection.  (For
-        details, see the Nixpkgs documentation.)  It allows you to set
-        package configuration options.
+        Global configuration for Nixpkgs.
+        The complete list of [Nixpkgs configuration options](https://nixos.org/manual/nixpkgs/unstable/#sec-config-options-reference) is in the [Nixpkgs manual section on global configuration](https://nixos.org/manual/nixpkgs/unstable/#chap-packageconfig).
 
-        Ignored when `nixpkgs.pkgs` is set.
+        Ignored when {option}`nixpkgs.pkgs` is set.
       '';
     };
 
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index a45507d5ee3cd..e92cbe31e25ac 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -265,6 +265,7 @@
   ./programs/skim.nix
   ./programs/slock.nix
   ./programs/sniffnet.nix
+  ./programs/soundmodem.nix
   ./programs/spacefm.nix
   ./programs/ssh.nix
   ./programs/starship.nix
@@ -286,8 +287,8 @@
   ./programs/virt-manager.nix
   ./programs/wavemon.nix
   ./programs/wayland/cardboard.nix
-  ./programs/wayland/labwc.nix
   ./programs/wayland/hyprland.nix
+  ./programs/wayland/labwc.nix
   ./programs/wayland/river.nix
   ./programs/wayland/sway.nix
   ./programs/wayland/waybar.nix
@@ -690,6 +691,7 @@
   ./services/misc/clipmenu.nix
   ./services/misc/confd.nix
   ./services/misc/cpuminer-cryptonight.nix
+  ./services/misc/db-rest.nix
   ./services/misc/devmon.nix
   ./services/misc/dictd.nix
   ./services/misc/disnix.nix
diff --git a/nixos/modules/programs/soundmodem.nix b/nixos/modules/programs/soundmodem.nix
new file mode 100644
index 0000000000000..59c1f2fb2dedd
--- /dev/null
+++ b/nixos/modules/programs/soundmodem.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.soundmodem;
+in
+{
+  options = {
+    programs.soundmodem = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to add Soundmodem to the global environment and configure a
+          wrapper for 'soundmodemconfig' for users in the 'soundmodem' group.
+        '';
+      };
+      package = mkPackageOption pkgs "soundmodem" { };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ soundmodem ];
+    users.groups.soundmodem = { };
+
+    security.wrappers.soundmodemconfig = {
+      source = "${cfg.package}/bin/soundmodemconfig";
+      owner = "root";
+      group = "soundmodem";
+      permissions = "u+rx,g+x";
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
index c3edba52433f6..30be56f8eeabe 100644
--- a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
@@ -203,6 +203,8 @@ in
             TOKEN = "${instance.token}";
           } // optionalAttrs (wantsPodman) {
             DOCKER_HOST = "unix:///run/podman/podman.sock";
+          } // {
+            HOME = "/var/lib/gitea-runner/${name}";
           };
           path = with pkgs; [
             coreutils
diff --git a/nixos/modules/services/misc/db-rest.nix b/nixos/modules/services/misc/db-rest.nix
new file mode 100644
index 0000000000000..fbf8b327af049
--- /dev/null
+++ b/nixos/modules/services/misc/db-rest.nix
@@ -0,0 +1,182 @@
+{ config, pkgs, lib, ... }:
+let
+  inherit (lib) mkOption types mkIf mkMerge mkDefault mkEnableOption mkPackageOption maintainers;
+  cfg = config.services.db-rest;
+in
+{
+  options = {
+    services.db-rest = {
+      enable = mkEnableOption "db-rest service";
+
+      user = mkOption {
+        type = types.str;
+        default = "db-rest";
+        description = "User account under which db-rest runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "db-rest";
+        description = "Group under which db-rest runs.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The host address the db-rest server should listen on.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = "The port the db-rest server should listen on.";
+      };
+
+      redis = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable caching with redis for db-rest.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Configure a local redis server for db-rest.";
+        };
+
+        host = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Redis host.";
+        };
+
+        port = mkOption {
+          type = with types; nullOr port;
+          default = null;
+          description = "Redis port.";
+        };
+
+        user = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Optional username used for authentication with redis.";
+        };
+
+        passwordFile = mkOption {
+          type = with types; nullOr path;
+          default = null;
+          example = "/run/keys/db-rest/pasword-redis-db";
+          description = "Path to a file containing the redis password.";
+        };
+
+        useSSL = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Use SSL if using a redis network connection.";
+        };
+      };
+
+      package = mkPackageOption pkgs "db-rest" { };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.redis.enable && !cfg.redis.createLocally) -> (cfg.redis.host != null && cfg.redis.port != null);
+        message = ''
+          {option}`services.db-rest.redis.createLocally` and redis network connection ({option}`services.db-rest.redis.host` or {option}`services.db-rest.redis.port`) enabled. Disable either of them.
+        '';
+      }
+      {
+        assertion = (cfg.redis.enable && !cfg.redis.createLocally) -> (cfg.redis.passwordFile != null);
+        message = ''
+          {option}`services.db-rest.redis.createLocally` is disabled, but {option}`services.db-rest.redis.passwordFile` is not set.
+        '';
+      }
+    ];
+
+    systemd.services.db-rest = mkMerge [
+      {
+        description = "db-rest service";
+        after = [ "network.target" ]
+          ++ lib.optional cfg.redis.createLocally "redis-db-rest.service";
+        requires = lib.optional cfg.redis.createLocally "redis-db-rest.service";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "simple";
+          Restart = "always";
+          RestartSec = 5;
+          WorkingDirectory = cfg.package;
+          User = cfg.user;
+          Group = cfg.group;
+          RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+          MemoryDenyWriteExecute = false;
+          LoadCredential = lib.optional (cfg.redis.enable && cfg.redis.passwordFile != null) "REDIS_PASSWORD:${cfg.redis.passwordFile}";
+          ExecStart = mkDefault "${cfg.package}/bin/db-rest";
+
+          RemoveIPC = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          ProtectClock = true;
+          ProtectKernelLogs = true;
+          ProtectControlGroups = true;
+          ProtectKernelModules = true;
+          PrivateMounts = true;
+          SystemCallArchitectures = "native";
+          ProtectHostname = true;
+          LockPersonality = true;
+          ProtectKernelTunables = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          RestrictNamespaces = true;
+          ProtectSystem = "strict";
+          ProtectProc = "invisible";
+          ProcSubset = "pid";
+          ProtectHome = true;
+          PrivateUsers = true;
+          PrivateTmp = true;
+          CapabilityBoundingSet = "";
+        };
+        environment = {
+          NODE_ENV = "production";
+          NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
+          HOSTNAME = cfg.host;
+          PORT = toString cfg.port;
+        };
+      }
+      (mkIf cfg.redis.enable (if cfg.redis.createLocally then
+        { environment.REDIS_URL = config.services.redis.servers.db-rest.unixSocket; }
+      else
+        {
+          script =
+            let
+              username = lib.optionalString (cfg.redis.user != null) (cfg.redis.user);
+              host = cfg.redis.host;
+              port = toString cfg.redis.port;
+              protocol = if cfg.redis.useSSL then "rediss" else "redis";
+            in
+            ''
+              export REDIS_URL="${protocol}://${username}:$(${config.systemd.package}/bin/systemd-creds cat REDIS_PASSWORD)@${host}:${port}"
+              exec ${cfg.package}/bin/db-rest
+            '';
+        }))
+    ];
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "db-rest") {
+        db-rest = {
+          isSystemUser = true;
+          group = cfg.group;
+        };
+      })
+      (lib.mkIf cfg.redis.createLocally { ${cfg.user}.extraGroups = [ "redis-db-rest" ]; })
+    ];
+
+    users.groups = lib.mkIf (cfg.group == "db-rest") { db-rest = { }; };
+
+    services.redis.servers.db-rest.enable = cfg.redis.enable && cfg.redis.createLocally;
+  };
+  meta.maintainers = with maintainers; [ marie ];
+}
diff --git a/nixos/modules/services/misc/ollama.nix b/nixos/modules/services/misc/ollama.nix
index b2c3de09015b9..948c8f17f9894 100644
--- a/nixos/modules/services/misc/ollama.nix
+++ b/nixos/modules/services/misc/ollama.nix
@@ -85,7 +85,7 @@ in
       };
       serviceConfig = {
         ExecStart = "${lib.getExe ollamaPackage} serve";
-        WorkingDirectory = "%S/ollama";
+        WorkingDirectory = cfg.home;
         StateDirectory = [ "ollama" ];
         DynamicUser = true;
       };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/redis.nix b/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
index 71f94a700efd9..ee7d87e8e6150 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
@@ -9,6 +9,7 @@ in
   port = 9121;
   serviceOpts = {
     serviceConfig = {
+      RestrictAddressFamilies = [ "AF_UNIX" ];
       ExecStart = ''
         ${pkgs.prometheus-redis-exporter}/bin/redis_exporter \
           -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
diff --git a/nixos/modules/services/networking/shadowsocks.nix b/nixos/modules/services/networking/shadowsocks.nix
index 84d7ece075fef..2f6f40f2b0f60 100644
--- a/nixos/modules/services/networking/shadowsocks.nix
+++ b/nixos/modules/services/networking/shadowsocks.nix
@@ -136,10 +136,16 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-    assertions = singleton
-      { assertion = cfg.password == null || cfg.passwordFile == null;
-        message = "Cannot use both password and passwordFile for shadowsocks-libev";
-      };
+    assertions = [
+      {
+        # xor, make sure either password or passwordFile be set.
+        # shadowsocks-libev not support plain/none encryption method
+        # which indicated that password must set.
+        assertion = let noPasswd = cfg.password == null; noPasswdFile = cfg.passwordFile == null;
+          in (noPasswd && !noPasswdFile) || (!noPasswd && noPasswdFile);
+        message = "Option `password` or `passwordFile` must be set and cannot be set simultaneously";
+      }
+    ];
 
     systemd.services.shadowsocks-libev = {
       description = "shadowsocks-libev Daemon";
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
index 3eebf340f9b20..eca498549df36 100644
--- a/nixos/modules/services/web-apps/akkoma.nix
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -772,6 +772,11 @@ in {
                     default = if lib.versionOlder config.system.stateVersion "24.05"
                               then "${httpConf.scheme}://${httpConf.host}:${builtins.toString httpConf.port}/media/"
                               else null;
+                    defaultText = literalExpression ''
+                      if lib.versionOlder config.system.stateVersion "24.05"
+                      then "$\{httpConf.scheme}://$\{httpConf.host}:$\{builtins.toString httpConf.port}/media/"
+                      else null;
+                    '';
                     description = ''
                       Base path which uploads will be stored at.
                       Whilst this can just be set to a subdirectory of the main domain, it is now recommended to use a different subdomain.
@@ -804,6 +809,7 @@ in {
                 enabled = mkOption {
                     type = types.bool;
                     default = false;
+                    defaultText = literalExpression "false";
                     description = ''
                       Whether to enable proxying of remote media through the instance's proxy.
                     '';
@@ -813,6 +819,11 @@ in {
                     default = if lib.versionOlder config.system.stateVersion "24.05"
                               then "${httpConf.scheme}://${httpConf.host}:${builtins.toString httpConf.port}/media/"
                               else null;
+                    defaultText = literalExpression ''
+                      if lib.versionOlder config.system.stateVersion "24.05"
+                      then "$\{httpConf.scheme}://$\{httpConf.host}:$\{builtins.toString httpConf.port}/media/"
+                      else null;
+                    '';
                     description = ''
                       Base path for the media proxy.
                       Whilst this can just be set to a subdirectory of the main domain, it is now recommended to use a different subdomain.
diff --git a/nixos/modules/services/x11/urserver.nix b/nixos/modules/services/x11/urserver.nix
index 0beb62eb766a3..30f8a9805cfbc 100644
--- a/nixos/modules/services/x11/urserver.nix
+++ b/nixos/modules/services/x11/urserver.nix
@@ -14,7 +14,7 @@ in {
       allowedUDPPorts = [ 9511 9512 ];
     };
 
-    systemd.user.services.urserver =  {
+    systemd.user.services.urserver = {
       description = ''
         Server for Unified Remote: The one-and-only remote for your computer.
       '';
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index fe02a97d6ff1e..a99fedaddd763 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -236,6 +236,7 @@ in {
   darling = handleTest ./darling.nix {};
   dae = handleTest ./dae.nix {};
   davis = handleTest ./davis.nix {};
+  db-rest = handleTest ./db-rest.nix {};
   dconf = handleTest ./dconf.nix {};
   deconz = handleTest ./deconz.nix {};
   deepin = handleTest ./deepin.nix {};
diff --git a/nixos/tests/db-rest.nix b/nixos/tests/db-rest.nix
new file mode 100644
index 0000000000000..9249da904acbe
--- /dev/null
+++ b/nixos/tests/db-rest.nix
@@ -0,0 +1,107 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "db-rest";
+  meta.maintainers = with pkgs.lib.maintainers; [ marie ];
+
+  nodes = {
+    database = {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.10"; prefixLength = 24; }
+          ];
+        };
+        firewall.allowedTCPPorts = [ 31638 ];
+      };
+
+      services.redis.servers.db-rest = {
+        enable = true;
+        bind = "0.0.0.0";
+        requirePass = "choochoo";
+        port = 31638;
+      };
+    };
+
+    serverWithTcp = { pkgs, ... }: {
+      environment = {
+        etc = {
+          "db-rest/password-redis-db".text = ''
+            choochoo
+          '';
+        };
+      };
+
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.11"; prefixLength = 24; }
+          ];
+        };
+        firewall.allowedTCPPorts = [ 3000 ];
+      };
+
+      services.db-rest = {
+        enable = true;
+        host = "0.0.0.0";
+        redis = {
+          enable = true;
+          createLocally = false;
+          host = "192.168.2.10";
+          port = 31638;
+          passwordFile = "/etc/db-rest/password-redis-db";
+          useSSL = false;
+        };
+      };
+    };
+
+    serverWithUnixSocket = { pkgs, ... }: {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.12"; prefixLength = 24; }
+          ];
+        };
+        firewall.allowedTCPPorts = [ 3000 ];
+      };
+
+      services.db-rest = {
+        enable = true;
+        host = "0.0.0.0";
+        redis = {
+          enable = true;
+          createLocally = true;
+        };
+      };
+    };
+
+    client = {
+      environment.systemPackages = [ pkgs.jq ];
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.13"; prefixLength = 24; }
+          ];
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("db-rest redis with TCP socket"):
+      database.wait_for_unit("redis-db-rest.service")
+      database.wait_for_open_port(31638)
+
+      serverWithTcp.wait_for_unit("db-rest.service")
+      serverWithTcp.wait_for_open_port(3000)
+
+      client.succeed("curl --fail --get http://192.168.2.11:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'")
+
+    with subtest("db-rest redis with Unix socket"):
+      serverWithUnixSocket.wait_for_unit("db-rest.service")
+      serverWithUnixSocket.wait_for_open_port(3000)
+
+      client.succeed("curl --fail --get http://192.168.2.12:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'")
+  '';
+})
diff --git a/nixos/tests/forgejo.nix b/nixos/tests/forgejo.nix
index b14df0a2c74f9..8b9ee46ff5d32 100644
--- a/nixos/tests/forgejo.nix
+++ b/nixos/tests/forgejo.nix
@@ -22,8 +22,27 @@ let
   '';
   signingPrivateKeyId = "4D642DE8B678C79D";
 
+  actionsWorkflowYaml = ''
+    run-name: dummy workflow
+    on:
+      push:
+    jobs:
+      cat:
+        runs-on: native
+        steps:
+          - uses: http://localhost:3000/test/checkout@main
+          - run: cat testfile
+  '';
+  # https://github.com/actions/checkout/releases
+  checkoutActionSource = pkgs.fetchFromGitHub {
+    owner = "actions";
+    repo = "checkout";
+    rev = "v4.1.1";
+    hash = "sha256-h2/UIp8IjPo3eE4Gzx52Fb7pcgG/Ww7u31w5fdKVMos=";
+  };
+
   supportedDbTypes = [ "mysql" "postgres" "sqlite3" ];
-  makeGForgejoTest = type: nameValuePair type (makeTest {
+  makeForgejoTest = type: nameValuePair type (makeTest {
     name = "forgejo-${type}";
     meta.maintainers = with maintainers; [ bendlas emilylange ];
 
@@ -36,21 +55,28 @@ let
           settings.service.DISABLE_REGISTRATION = true;
           settings."repository.signing".SIGNING_KEY = signingPrivateKeyId;
           settings.actions.ENABLED = true;
+          settings.repository = {
+            ENABLE_PUSH_CREATE_USER = true;
+            DEFAULT_PUSH_CREATE_PRIVATE = false;
+          };
         };
-        environment.systemPackages = [ config.services.forgejo.package pkgs.gnupg pkgs.jq pkgs.file ];
+        environment.systemPackages = [ config.services.forgejo.package pkgs.gnupg pkgs.jq pkgs.file pkgs.htmlq ];
         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";
+          configuration.services.gitea-actions-runner = {
+            package = pkgs.forgejo-runner;
+            instances."test" = {
+              enable = true;
+              name = "ci";
+              url = "http://localhost:3000";
+              labels = [
+                # type ":host" does not depend on docker/podman/lxc
+                "native:host"
+              ];
+              tokenFile = "/var/lib/forgejo/runner_token";
+            };
           };
         };
         specialisation.dump = {
@@ -62,11 +88,20 @@ let
           };
         };
       };
-      client1 = { config, pkgs, ... }: {
-        environment.systemPackages = [ pkgs.git ];
-      };
-      client2 = { config, pkgs, ... }: {
-        environment.systemPackages = [ pkgs.git ];
+      client = { ... }: {
+        programs.git = {
+          enable = true;
+          config = {
+            user.email = "test@localhost";
+            user.name = "test";
+            init.defaultBranch = "main";
+          };
+        };
+        programs.ssh.extraConfig = ''
+          Host *
+            StrictHostKeyChecking no
+            IdentityFile ~/.ssh/privk
+        '';
       };
     };
 
@@ -75,26 +110,23 @@ let
         inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
         serverSystem = nodes.server.system.build.toplevel;
         dumpFile = with nodes.server.specialisation.dump.configuration.services.forgejo.dump; "${backupDir}/${file}";
+        remoteUri = "forgejo@server:test/repo";
+        remoteUriCheckoutAction = "forgejo@server:test/checkout";
       in
       ''
         import json
-        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}")
+        client.succeed("mkdir -p ~/.ssh")
+        client.succeed("(umask 0077; cat ${snakeOilPrivateKey} > ~/.ssh/privk)")
+
+        client.succeed("mkdir /tmp/repo")
+        client.succeed("git -C /tmp/repo init")
+        client.succeed("echo 'hello world' > /tmp/repo/testfile")
+        client.succeed("git -C /tmp/repo add .")
+        client.succeed("git -C /tmp/repo commit -m 'Initial import'")
+        client.succeed("git -C /tmp/repo remote add origin ${remoteUri}")
 
         server.wait_for_unit("forgejo.service")
         server.wait_for_open_port(3000)
@@ -143,18 +175,14 @@ let
             + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\'''
         )
 
-        client1.succeed(
-            f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git -C /tmp/repo push origin master"
-        )
+        client.succeed("git -C /tmp/repo push origin main")
 
-        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"')
+        client.succeed("git clone ${remoteUri} /tmp/repo-clone")
+        print(client.succeed("ls -lash /tmp/repo-clone"))
+        assert "hello world" == client.succeed("cat /tmp/repo-clone/testfile").strip()
 
         with subtest("Testing git protocol version=2 over ssh"):
-            git_protocol = client2.succeed(f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' GIT_TRACE2_EVENT=true git -C repo fetch |& grep negotiated-version")
+            git_protocol = client.succeed("GIT_TRACE2_EVENT=true git -C /tmp/repo-clone fetch |& grep negotiated-version")
             version = json.loads(git_protocol).get("value")
             assert version == "2", f"git did not negotiate protocol version 2, but version {version} instead."
 
@@ -164,7 +192,7 @@ let
             timeout=10
         )
 
-        with subtest("Testing runner registration"):
+        with subtest("Testing runner registration and action workflow"):
             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"
             )
@@ -172,6 +200,52 @@ let
             server.wait_for_unit("gitea-runner-test.service")
             server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'")
 
+            # enable actions feature for this repository, defaults to disabled
+            server.succeed(
+                "curl --fail -X PATCH http://localhost:3000/api/v1/repos/test/repo "
+                + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
+                + f"-H 'Authorization: token {api_token}'"
+                + ' -d \'{"has_actions":true}\'''
+            )
+
+            # mirror "actions/checkout" action
+            client.succeed("cp -R ${checkoutActionSource}/ /tmp/checkout")
+            client.succeed("git -C /tmp/checkout init")
+            client.succeed("git -C /tmp/checkout add .")
+            client.succeed("git -C /tmp/checkout commit -m 'Initial import'")
+            client.succeed("git -C /tmp/checkout remote add origin ${remoteUriCheckoutAction}")
+            client.succeed("git -C /tmp/checkout push origin main")
+
+            # push workflow to initial repo
+            client.succeed("mkdir -p /tmp/repo/.forgejo/workflows")
+            client.succeed("cp ${pkgs.writeText "dummy-workflow.yml" actionsWorkflowYaml} /tmp/repo/.forgejo/workflows/")
+            client.succeed("git -C /tmp/repo add .")
+            client.succeed("git -C /tmp/repo commit -m 'Add dummy workflow'")
+            client.succeed("git -C /tmp/repo push origin main")
+
+            def poll_workflow_action_status(_) -> bool:
+                output = server.succeed(
+                    "curl --fail http://localhost:3000/test/repo/actions | "
+                    + 'htmlq ".flex-item-leading span" --attribute "data-tooltip-content"'
+                ).strip()
+
+                # values taken from https://codeberg.org/forgejo/forgejo/src/commit/af47c583b4fb3190fa4c4c414500f9941cc02389/options/locale/locale_en-US.ini#L3649-L3661
+                if output in [ "Failure", "Canceled", "Skipped", "Blocked" ]:
+                    raise Exception(f"Workflow status is '{output}', which we consider failed.")
+                    server.log(f"Command returned '{output}', which we consider failed.")
+
+                elif output in [ "Unknown", "Waiting", "Running", "" ]:
+                    server.log(f"Workflow status is '{output}'. Waiting some more...")
+                    return False
+
+                elif output in [ "Success" ]:
+                    return True
+
+                raise Exception(f"Workflow status is '{output}', which we don't know. Value mappings likely need updating.")
+
+            with server.nested("Waiting for the workflow run to be successful"):
+                retry(poll_workflow_action_status)
+
         with subtest("Testing backup service"):
             server.succeed("${serverSystem}/specialisation/dump/bin/switch-to-configuration test")
             server.systemctl("start forgejo-dump")
@@ -181,4 +255,4 @@ let
   });
 in
 
-listToAttrs (map makeGForgejoTest supportedDbTypes)
+listToAttrs (map makeForgejoTest supportedDbTypes)
diff --git a/nixos/tests/teleport.nix b/nixos/tests/teleport.nix
index d68917c6c7acb..2fb347155759a 100644
--- a/nixos/tests/teleport.nix
+++ b/nixos/tests/teleport.nix
@@ -9,8 +9,8 @@ with import ../lib/testing-python.nix { inherit system pkgs; };
 let
   packages = with pkgs; {
     "default" = teleport;
-    "12" = teleport_12;
     "13" = teleport_13;
+    "14" = teleport_14;
   };
 
   minimal = package: {