about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--maintainers/maintainer-list.nix10
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix5
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix9
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager-webhook-logger.nix70
-rw-r--r--nixos/tests/all-tests.nix3
-rw-r--r--nixos/tests/home-assistant.nix8
-rw-r--r--nixos/tests/prometheus/alertmanager.nix148
-rw-r--r--nixos/tests/prometheus/config-reload.nix116
-rw-r--r--nixos/tests/prometheus/default.nix13
-rw-r--r--nixos/tests/prometheus/federation.nix213
-rw-r--r--nixos/tests/prometheus/prometheus-pair.nix87
-rw-r--r--nixos/tests/prometheus/pushgateway.nix94
-rw-r--r--nixos/tests/prometheus/remote-write.nix73
-rw-r--r--nixos/tests/thanos.nix (renamed from nixos/tests/prometheus.nix)58
-rw-r--r--pkgs/applications/editors/emacs/elisp-packages/elpa-devel-generated.nix6
-rw-r--r--pkgs/applications/editors/emacs/elisp-packages/elpa-devel-packages.nix20
-rw-r--r--pkgs/applications/editors/emacs/elisp-packages/elpa-packages.nix20
-rw-r--r--pkgs/applications/editors/emacs/sources.nix6
-rw-r--r--pkgs/build-support/dotnet/build-dotnet-module/hooks/dotnet-fixup-hook.sh13
-rw-r--r--pkgs/by-name/al/alertmanager-webhook-logger/package.nix33
-rw-r--r--pkgs/by-name/gp/gpsprune/package.nix4
-rw-r--r--pkgs/by-name/mi/miru/darwin.nix33
-rw-r--r--pkgs/by-name/mi/miru/linux.nix31
-rw-r--r--pkgs/by-name/mi/miru/package.nix43
-rw-r--r--pkgs/by-name/so/solo5/package.nix6
-rw-r--r--pkgs/development/python-modules/textual/default.nix4
-rw-r--r--pkgs/development/tools/language-servers/csharp-ls/default.nix4
-rw-r--r--pkgs/servers/home-assistant/custom-components/default.nix2
-rw-r--r--pkgs/servers/home-assistant/custom-components/spook/default.nix38
-rw-r--r--pkgs/servers/home-assistant/custom-components/spook/remove-sub-integration-symlink-hack.patch109
-rw-r--r--pkgs/servers/janus-gateway/default.nix4
-rw-r--r--pkgs/servers/monitoring/prometheus/alertmanager.nix3
-rw-r--r--pkgs/servers/monitoring/prometheus/default.nix8
-rw-r--r--pkgs/servers/monitoring/prometheus/pushgateway.nix9
-rw-r--r--pkgs/servers/monitoring/thanos/default.nix2
-rw-r--r--pkgs/tools/misc/panoply/default.nix4
37 files changed, 1189 insertions, 121 deletions
diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix
index 6568e3a61e3a7..fce218ca723a3 100644
--- a/maintainers/maintainer-list.nix
+++ b/maintainers/maintainer-list.nix
@@ -9848,6 +9848,11 @@
     githubId = 1918771;
     name = "Joe Doyle";
   };
+  jpds = {
+    github = "jpds";
+    githubId = 29158971;
+    name = "Jonathan Davies";
+  };
   jpentland = {
     email = "joe.pentland@gmail.com";
     github = "jpentland";
@@ -10618,6 +10623,11 @@
     github = "kkharji";
     githubId = 65782666;
   };
+  kkoniuszy = {
+    name = "Kacper Koniuszy";
+    github = "kkoniuszy";
+    githubId = 120419423;
+  };
   klden = {
     name = "Kenzyme Le";
     email = "kl@kenzymele.com";
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index d412e414f68b5..876e40983c1e5 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -880,6 +880,7 @@
   ./services/monitoring/osquery.nix
   ./services/monitoring/parsedmarc.nix
   ./services/monitoring/prometheus/alertmanager-irc-relay.nix
+  ./services/monitoring/prometheus/alertmanager-webhook-logger.nix
   ./services/monitoring/prometheus/alertmanager.nix
   ./services/monitoring/prometheus/default.nix
   ./services/monitoring/prometheus/exporters.nix
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index d94adfb4aa1c7..c58a31539ed81 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -518,8 +518,9 @@ in {
           # recreate symlinks for desired components
           declare -a components=(${escapeShellArgs cfg.customComponents})
           for component in "''${components[@]}"; do
-            path="$(dirname $(find "$component" -name "manifest.json"))"
-            ln -fns "$path" "${cfg.configDir}/custom_components/"
+            readarray -t manifests < <(find "$component" -name manifest.json)
+            readarray -t paths < <(dirname "''${manifests[@]}")
+            ln -fns "''${paths[@]}" "${cfg.configDir}/custom_components/"
           done
         '';
       in
diff --git a/nixos/modules/services/misc/sourcehut/service.nix b/nixos/modules/services/misc/sourcehut/service.nix
index ce5a0e78627c1..3507a49ea13a8 100644
--- a/nixos/modules/services/misc/sourcehut/service.nix
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -324,7 +324,8 @@ in
               };
               preStart =
                 let
-                  version = pkgs.sourcehut.${srvsrht}.version;
+                  package = pkgs.sourcehut.${srvsrht};
+                  version = package.version;
                   stateDir = "/var/lib/sourcehut/${srvsrht}";
                 in
                 mkBefore ''
@@ -336,14 +337,14 @@ in
                   if test ! -e ${stateDir}/db; then
                     # Setup the initial database.
                     # Note that it stamps the alembic head afterward
-                    ${cfg.python}/bin/${srvsrht}-initdb
+                    ${package}/bin/${srvsrht}-initdb
                     echo ${version} >${stateDir}/db
                   fi
 
                   ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
                     if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
                       # Manage schema migrations using alembic
-                      ${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
+                      ${package}/bin/${srvsrht}-migrate -a upgrade head
                       echo ${version} >${stateDir}/db
                     fi
                   ''}
@@ -389,7 +390,7 @@ in
               after = [ "network.target" "${srvsrht}.service" ];
               serviceConfig = {
                 Type = "oneshot";
-                ExecStart = "${cfg.python}/bin/${timerName}";
+                ExecStart = "${pkgs.sourcehut.${srvsrht}}/bin/${timerName}";
               };
             }
             (timer.service or { })
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager-webhook-logger.nix b/nixos/modules/services/monitoring/prometheus/alertmanager-webhook-logger.nix
new file mode 100644
index 0000000000000..b4307a76e1b02
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager-webhook-logger.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.alertmanagerWebhookLogger;
+in
+{
+  options.services.prometheus.alertmanagerWebhookLogger = {
+    enable = mkEnableOption "Alertmanager Webhook Logger";
+
+    package = mkPackageOption pkgs "alertmanager-webhook-logger" { };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = "Extra command line options to pass to alertmanager-webhook-logger.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.alertmanager-webhook-logger = {
+      description = "Alertmanager Webhook Logger";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/alertmanager-webhook-logger \
+          ${escapeShellArgs cfg.extraFlags}
+        '';
+
+        DynamicUser = true;
+        NoNewPrivileges = true;
+
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation"
+          "~@privileged"
+          "~@reboot"
+          "~@setuid"
+          "~@swap"
+        ];
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.jpds ];
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index b1f7d1917ed45..1db9f702e2416 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -775,7 +775,7 @@ in {
   printing-service = handleTest ./printing.nix { socket = false; };
   private-gpt = handleTest ./private-gpt.nix {};
   privoxy = handleTest ./privoxy.nix {};
-  prometheus = handleTest ./prometheus.nix {};
+  prometheus = handleTest ./prometheus {};
   prometheus-exporters = handleTest ./prometheus-exporters.nix {};
   prosody = handleTest ./xmpp/prosody.nix {};
   prosody-mysql = handleTest ./xmpp/prosody-mysql.nix {};
@@ -968,6 +968,7 @@ in {
   teleport = handleTest ./teleport.nix {};
   thelounge = handleTest ./thelounge.nix {};
   terminal-emulators = handleTest ./terminal-emulators.nix {};
+  thanos = handleTest ./thanos.nix {};
   tiddlywiki = handleTest ./tiddlywiki.nix {};
   tigervnc = handleTest ./tigervnc.nix {};
   timescaledb = handleTest ./timescaledb.nix {};
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 05fb2fa1e06aa..47902fa4e1340 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -44,6 +44,8 @@ in {
       # test loading custom components
       customComponents = with pkgs.home-assistant-custom-components; [
         prometheus_sensor
+        # tests loading multiple components from a single package
+        spook
       ];
 
       # test loading lovelace modules
@@ -179,7 +181,8 @@ in {
 
     with subtest("Check that custom components get installed"):
         hass.succeed("test -f ${configDir}/custom_components/prometheus_sensor/manifest.json")
-        hass.wait_until_succeeds("journalctl -u home-assistant.service | grep -q 'We found a custom integration prometheus_sensor which has not been tested by Home Assistant'")
+        for integration in ("prometheus_sensor", "spook", "spook_inverse"):
+            hass.wait_until_succeeds(f"journalctl -u home-assistant.service | grep -q 'We found a custom integration {integration} which has not been tested by Home Assistant'")
 
     with subtest("Check that lovelace modules are referenced and fetchable"):
         hass.succeed("grep -q 'mini-graph-card-bundle.js' '${configDir}/configuration.yaml'")
@@ -228,7 +231,8 @@ in {
         cursor = get_journal_cursor()
         hass.succeed("${system}/specialisation/removeCustomThings/bin/switch-to-configuration test")
         hass.fail("grep -q 'mini-graph-card-bundle.js' '${configDir}/ui-lovelace.yaml'")
-        hass.fail("test -f ${configDir}/custom_components/prometheus_sensor/manifest.json")
+        for integration in ("prometheus_sensor", "spook", "spook_inverse"):
+            hass.fail(f"test -f ${configDir}/custom_components/{integration}/manifest.json")
         wait_for_homeassistant(cursor)
 
     with subtest("Check that no errors were logged"):
diff --git a/nixos/tests/prometheus/alertmanager.nix b/nixos/tests/prometheus/alertmanager.nix
new file mode 100644
index 0000000000000..feda8d8fc2bcc
--- /dev/null
+++ b/nixos/tests/prometheus/alertmanager.nix
@@ -0,0 +1,148 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "prometheus-alertmanager";
+
+  nodes = {
+    prometheus = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        alertmanagers = [
+          {
+            scheme = "http";
+            static_configs = [
+              {
+                targets = [
+                  "alertmanager:${toString config.services.prometheus.alertmanager.port}"
+                ];
+              }
+            ];
+          }
+        ];
+
+        rules = [
+          ''
+            groups:
+              - name: test
+                rules:
+                  - alert: InstanceDown
+                    expr: up == 0
+                    for: 5s
+                    labels:
+                      severity: page
+                    annotations:
+                      summary: "Instance {{ $labels.instance }} down"
+          ''
+        ];
+
+        scrapeConfigs = [
+          {
+            job_name = "alertmanager";
+            static_configs = [
+              {
+                targets = [
+                  "alertmanager:${toString config.services.prometheus.alertmanager.port}"
+                ];
+              }
+            ];
+          }
+          {
+            job_name = "node";
+            static_configs = [
+              {
+                targets = [
+                  "node:${toString config.services.prometheus.exporters.node.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    alertmanager = { config, pkgs, ... }: {
+      services.prometheus.alertmanager = {
+        enable = true;
+        openFirewall = true;
+
+        configuration = {
+          global = {
+            resolve_timeout = "1m";
+          };
+
+          route = {
+            # Root route node
+            receiver = "test";
+            group_by = ["..."];
+            continue = false;
+            group_wait = "1s";
+            group_interval = "15s";
+            repeat_interval = "24h";
+          };
+
+          receivers = [
+            {
+              name = "test";
+              webhook_configs = [
+                {
+                  url = "http://logger:6725";
+                  send_resolved = true;
+                  max_alerts = 0;
+                }
+              ];
+            }
+          ];
+        };
+      };
+    };
+
+    logger = { config, pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 6725 ];
+
+      services.prometheus.alertmanagerWebhookLogger.enable = true;
+    };
+  };
+
+  testScript = ''
+    alertmanager.wait_for_unit("alertmanager")
+    alertmanager.wait_for_open_port(9093)
+    alertmanager.wait_until_succeeds("curl -s http://127.0.0.1:9093/-/ready")
+    #alertmanager.wait_until_succeeds("journalctl -o cat -u alertmanager.service | grep 'version=${pkgs.prometheus-alertmanager.version}'")
+
+    logger.wait_for_unit("alertmanager-webhook-logger")
+    logger.wait_for_open_port(6725)
+
+    prometheus.wait_for_unit("prometheus")
+    prometheus.wait_for_open_port(9090)
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(up\{job=\"alertmanager\"\}==1)' | "
+      + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=sum(alertmanager_build_info)%20by%20(version)' | "
+      + "jq '.data.result[0].metric.version' | grep '\"${pkgs.prometheus-alertmanager.version}\"'"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(up\{job=\"node\"\}!=1)' | "
+      + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=alertmanager_notifications_total\{integration=\"webhook\"\}' | "
+      + "jq '.data.result[0].value[1]' | grep -v '\"0\"'"
+    )
+
+    logger.wait_until_succeeds(
+      "journalctl -o cat -u alertmanager-webhook-logger.service | grep '\"alertname\":\"InstanceDown\"'"
+    )
+  '';
+})
diff --git a/nixos/tests/prometheus/config-reload.nix b/nixos/tests/prometheus/config-reload.nix
new file mode 100644
index 0000000000000..786668c624ea9
--- /dev/null
+++ b/nixos/tests/prometheus/config-reload.nix
@@ -0,0 +1,116 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "prometheus-config-reload";
+
+  nodes = {
+    prometheus = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        enableReload = true;
+        globalConfig.scrape_interval = "2s";
+        scrapeConfigs = [
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "prometheus:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+
+      specialisation = {
+        "prometheus-config-change" = {
+          configuration = {
+            environment.systemPackages = [ pkgs.yq ];
+
+            # This configuration just adds a new prometheus job
+            # to scrape the node_exporter metrics of the s3 machine.
+            services.prometheus = {
+              scrapeConfigs = [
+                {
+                  job_name = "node";
+                  static_configs = [
+                    {
+                      targets = [ "node:${toString config.services.prometheus.exporters.node.port}" ];
+                    }
+                  ];
+                }
+              ];
+            };
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    prometheus.wait_for_unit("prometheus")
+    prometheus.wait_for_open_port(9090)
+
+    # Check if switching to a NixOS configuration that changes the prometheus
+    # configuration reloads (instead of restarts) prometheus before the switch
+    # finishes successfully:
+    with subtest("config change reloads prometheus"):
+      import json
+      # We check if prometheus has finished reloading by looking for the message
+      # "Completed loading of configuration file" in the journal between the start
+      # and finish of switching to the new NixOS configuration.
+      #
+      # To mark the start we record the journal cursor before starting the switch:
+      cursor_before_switching = json.loads(
+          prometheus.succeed("journalctl -n1 -o json --output-fields=__CURSOR")
+      )["__CURSOR"]
+
+      # Now we switch:
+      prometheus_config_change = prometheus.succeed(
+          "readlink /run/current-system/specialisation/prometheus-config-change"
+      ).strip()
+      prometheus.succeed(prometheus_config_change + "/bin/switch-to-configuration test")
+
+      # Next we retrieve all logs since the start of switching:
+      logs_after_starting_switching = prometheus.succeed(
+          """
+            journalctl --after-cursor='{cursor_before_switching}' -o json --output-fields=MESSAGE
+          """.format(
+              cursor_before_switching=cursor_before_switching
+          )
+      )
+
+      # Finally we check if the message "Completed loading of configuration file"
+      # occurs before the "finished switching to system configuration" message:
+      finished_switching_msg = (
+          "finished switching to system configuration " + prometheus_config_change
+      )
+      reloaded_before_switching_finished = False
+      finished_switching = False
+      for log_line in logs_after_starting_switching.split("\n"):
+          msg = json.loads(log_line)["MESSAGE"]
+          if "Completed loading of configuration file" in msg:
+              reloaded_before_switching_finished = True
+          if msg == finished_switching_msg:
+              finished_switching = True
+              break
+
+      assert reloaded_before_switching_finished
+      assert finished_switching
+
+      # Check if the reloaded config includes the new node job:
+      prometheus.succeed(
+        """
+          curl -sf http://127.0.0.1:9090/api/v1/status/config \
+            | jq -r .data.yaml \
+            | yq '.scrape_configs | any(.job_name == "node")' \
+            | grep true
+        """
+      )
+  '';
+})
diff --git a/nixos/tests/prometheus/default.nix b/nixos/tests/prometheus/default.nix
new file mode 100644
index 0000000000000..133922a453c05
--- /dev/null
+++ b/nixos/tests/prometheus/default.nix
@@ -0,0 +1,13 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+
+{
+  alertmanager = import ./alertmanager.nix { inherit system pkgs; };
+  config-reload = import ./config-reload.nix { inherit system pkgs; };
+  federation = import ./federation.nix { inherit system pkgs; };
+  prometheus-pair = import ./prometheus-pair.nix { inherit system pkgs; };
+  pushgateway = import ./pushgateway.nix { inherit system pkgs; };
+  remote-write = import ./remote-write.nix { inherit system pkgs; };
+}
diff --git a/nixos/tests/prometheus/federation.nix b/nixos/tests/prometheus/federation.nix
new file mode 100644
index 0000000000000..0f05166c8f5da
--- /dev/null
+++ b/nixos/tests/prometheus/federation.nix
@@ -0,0 +1,213 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "prometheus-federation";
+
+  nodes = {
+    global1 = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        scrapeConfigs = [
+          {
+            job_name = "federate";
+            honor_labels = true;
+            metrics_path = "/federate";
+
+            params = {
+              "match[]" = [
+                "{job=\"node\"}"
+                "{job=\"prometheus\"}"
+              ];
+            };
+
+            static_configs = [
+              {
+                targets = [
+                  "prometheus1:${toString config.services.prometheus.port}"
+                  "prometheus2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "global1:${toString config.services.prometheus.port}"
+                  "global2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    global2 = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        scrapeConfigs = [
+          {
+            job_name = "federate";
+            honor_labels = true;
+            metrics_path = "/federate";
+
+            params = {
+              "match[]" = [
+                "{job=\"node\"}"
+                "{job=\"prometheus\"}"
+              ];
+            };
+
+            static_configs = [
+              {
+                targets = [
+                  "prometheus1:${toString config.services.prometheus.port}"
+                  "prometheus2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "global1:${toString config.services.prometheus.port}"
+                  "global2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    prometheus1 = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        scrapeConfigs = [
+          {
+            job_name = "node";
+            static_configs = [
+              {
+                targets = [
+                  "node1:${toString config.services.prometheus.exporters.node.port}"
+                ];
+              }
+            ];
+          }
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "prometheus1:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    prometheus2 = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        scrapeConfigs = [
+          {
+            job_name = "node";
+            static_configs = [
+              {
+                targets = [
+                  "node2:${toString config.services.prometheus.exporters.node.port}"
+                ];
+              }
+            ];
+          }
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "prometheus2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    node1 = { config, pkgs, ... }: {
+      services.prometheus.exporters.node = {
+        enable = true;
+        openFirewall = true;
+      };
+    };
+
+    node2 = { config, pkgs, ... }: {
+      services.prometheus.exporters.node = {
+        enable = true;
+        openFirewall = true;
+      };
+    };
+  };
+
+  testScript = ''
+    for machine in node1, node2:
+      machine.wait_for_unit("prometheus-node-exporter")
+      machine.wait_for_open_port(9100)
+
+    for machine in prometheus1, prometheus2, global1, global2:
+      machine.wait_for_unit("prometheus")
+      machine.wait_for_open_port(9090)
+
+    # Verify both servers got the same data from the exporter
+    for machine in prometheus1, prometheus2:
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(up\{job=\"node\"\})' | "
+        + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+      )
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(prometheus_build_info)' | "
+        + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+      )
+
+    for machine in global1, global2:
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(up\{job=\"node\"\})' | "
+        + "jq '.data.result[0].value[1]' | grep '\"2\"'"
+      )
+
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(prometheus_build_info)' | "
+        + "jq '.data.result[0].value[1]' | grep '\"4\"'"
+      )
+  '';
+})
diff --git a/nixos/tests/prometheus/prometheus-pair.nix b/nixos/tests/prometheus/prometheus-pair.nix
new file mode 100644
index 0000000000000..3ac70ca0403ec
--- /dev/null
+++ b/nixos/tests/prometheus/prometheus-pair.nix
@@ -0,0 +1,87 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "prometheus-pair";
+
+  nodes = {
+    prometheus1 = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+        scrapeConfigs = [
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "prometheus1:${toString config.services.prometheus.port}"
+                  "prometheus2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    prometheus2 = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+        scrapeConfigs = [
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [
+                  "prometheus1:${toString config.services.prometheus.port}"
+                  "prometheus2:${toString config.services.prometheus.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+  };
+
+  testScript = ''
+    for machine in prometheus1, prometheus2:
+      machine.wait_for_unit("prometheus")
+      machine.wait_for_open_port(9090)
+      machine.wait_until_succeeds("journalctl -o cat -u prometheus.service | grep 'version=${pkgs.prometheus.version}'")
+      machine.wait_until_succeeds("curl -sSf http://localhost:9090/-/healthy")
+
+    # Prometheii ready - run some queries
+    for machine in prometheus1, prometheus2:
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=prometheus_build_info\{instance=\"prometheus1:9090\",version=\"${pkgs.prometheus.version}\"\}' | "
+        + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+      )
+
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=prometheus_build_info\{instance=\"prometheus1:9090\"\}' | "
+        + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+      )
+
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=sum(prometheus_build_info)%20by%20(version)' | "
+        + "jq '.data.result[0].metric.version' | grep '\"${pkgs.prometheus.version}\"'"
+      )
+
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=sum(prometheus_build_info)%20by%20(version)' | "
+        + "jq '.data.result[0].value[1]' | grep '\"2\"'"
+      )
+
+    prometheus1.log(prometheus1.succeed("systemd-analyze security prometheus.service | grep -v '✓'"))
+  '';
+})
diff --git a/nixos/tests/prometheus/pushgateway.nix b/nixos/tests/prometheus/pushgateway.nix
new file mode 100644
index 0000000000000..7904c8bf45b04
--- /dev/null
+++ b/nixos/tests/prometheus/pushgateway.nix
@@ -0,0 +1,94 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "prometheus-pushgateway";
+
+  nodes = {
+    prometheus = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        scrapeConfigs = [
+          {
+            job_name = "pushgateway";
+            static_configs = [
+              {
+                targets = [
+                  "pushgateway:9091"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    pushgateway = { config, pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 9091 ];
+
+      services.prometheus.pushgateway = {
+        enable = true;
+      };
+    };
+
+    client = { config, pkgs, ... }: {
+    };
+  };
+
+  testScript = ''
+    pushgateway.wait_for_unit("pushgateway")
+    pushgateway.wait_for_open_port(9091)
+    pushgateway.wait_until_succeeds("curl -s http://127.0.0.1:9091/-/ready")
+    pushgateway.wait_until_succeeds("journalctl -o cat -u pushgateway.service | grep 'version=${pkgs.prometheus-pushgateway.version}'")
+
+    prometheus.wait_for_unit("prometheus")
+    prometheus.wait_for_open_port(9090)
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=count(up\{job=\"pushgateway\"\})' | "
+      + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=sum(pushgateway_build_info)%20by%20(version)' | "
+      + "jq '.data.result[0].metric.version' | grep '\"${pkgs.prometheus-pushgateway.version}\"'"
+    )
+
+    client.wait_for_unit("network-online.target")
+
+    # Add a metric and check in Prometheus
+    client.wait_until_succeeds(
+      "echo 'some_metric 3.14' | curl --data-binary @- http://pushgateway:9091/metrics/job/some_job"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=some_metric' | "
+      + "jq '.data.result[0].value[1]' | grep '\"3.14\"'"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=absent(some_metric)' | "
+      + "jq '.data.result[0].value[1]' | grep 'null'"
+    )
+
+    # Delete the metric, check not in Prometheus
+    client.wait_until_succeeds(
+      "curl -X DELETE http://pushgateway:9091/metrics/job/some_job"
+    )
+
+    prometheus.wait_until_fails(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=some_metric' | "
+      + "jq '.data.result[0].value[1]' | grep '\"3.14\"'"
+    )
+
+    prometheus.wait_until_succeeds(
+      "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=absent(some_metric)' | "
+      + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+    )
+  '';
+})
diff --git a/nixos/tests/prometheus/remote-write.nix b/nixos/tests/prometheus/remote-write.nix
new file mode 100644
index 0000000000000..24092b9fb88da
--- /dev/null
+++ b/nixos/tests/prometheus/remote-write.nix
@@ -0,0 +1,73 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "prometheus-remote-write";
+
+  nodes = {
+    receiver = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        extraFlags = [ "--web.enable-remote-write-receiver" ];
+      };
+    };
+
+    prometheus = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+
+      networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ];
+
+      services.prometheus = {
+        enable = true;
+        globalConfig.scrape_interval = "2s";
+
+        remoteWrite = [
+          {
+            url = "http://receiver:9090/api/v1/write";
+          }
+        ];
+
+        scrapeConfigs = [
+          {
+            job_name = "node";
+            static_configs = [
+              {
+                targets = [
+                  "node:${toString config.services.prometheus.exporters.node.port}"
+                ];
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    node = { config, pkgs, ... }: {
+      services.prometheus.exporters.node = {
+        enable = true;
+        openFirewall = true;
+      };
+    };
+  };
+
+  testScript = ''
+    node.wait_for_unit("prometheus-node-exporter")
+    node.wait_for_open_port(9100)
+
+    for machine in prometheus, receiver:
+      machine.wait_for_unit("prometheus")
+      machine.wait_for_open_port(9090)
+
+    # Verify both servers got the same data from the exporter
+    for machine in prometheus, receiver:
+      machine.wait_until_succeeds(
+        "curl -sf 'http://127.0.0.1:9090/api/v1/query?query=node_exporter_build_info\{instance=\"node:9100\"\}' | "
+        + "jq '.data.result[0].value[1]' | grep '\"1\"'"
+      )
+  '';
+})
diff --git a/nixos/tests/prometheus.nix b/nixos/tests/thanos.nix
index 0111273893775..5bdfab7b3573f 100644
--- a/nixos/tests/prometheus.nix
+++ b/nixos/tests/thanos.nix
@@ -212,8 +212,6 @@ in import ./make-test-python.nix {
   };
 
   testScript = { nodes, ... } : ''
-    import json
-
     # Before starting the other machines we first make sure that our S3 service is online
     # and has a bucket added for thanos:
     s3.start()
@@ -289,61 +287,5 @@ in import ./make-test-python.nix {
         + "jq .thanos.labels.some_label | "
         + "grep 'required by thanos'"
     )
-
-    # Check if switching to a NixOS configuration that changes the prometheus
-    # configuration reloads (instead of restarts) prometheus before the switch
-    # finishes successfully:
-    with subtest("config change reloads prometheus"):
-        # We check if prometheus has finished reloading by looking for the message
-        # "Completed loading of configuration file" in the journal between the start
-        # and finish of switching to the new NixOS configuration.
-        #
-        # To mark the start we record the journal cursor before starting the switch:
-        cursor_before_switching = json.loads(
-            prometheus.succeed("journalctl -n1 -o json --output-fields=__CURSOR")
-        )["__CURSOR"]
-
-        # Now we switch:
-        prometheus_config_change = prometheus.succeed(
-            "readlink /run/current-system/specialisation/prometheus-config-change"
-        ).strip()
-        prometheus.succeed(prometheus_config_change + "/bin/switch-to-configuration test")
-
-        # Next we retrieve all logs since the start of switching:
-        logs_after_starting_switching = prometheus.succeed(
-            """
-              journalctl --after-cursor='{cursor_before_switching}' -o json --output-fields=MESSAGE
-            """.format(
-                cursor_before_switching=cursor_before_switching
-            )
-        )
-
-        # Finally we check if the message "Completed loading of configuration file"
-        # occurs before the "finished switching to system configuration" message:
-        finished_switching_msg = (
-            "finished switching to system configuration " + prometheus_config_change
-        )
-        reloaded_before_switching_finished = False
-        finished_switching = False
-        for log_line in logs_after_starting_switching.split("\n"):
-            msg = json.loads(log_line)["MESSAGE"]
-            if "Completed loading of configuration file" in msg:
-                reloaded_before_switching_finished = True
-            if msg == finished_switching_msg:
-                finished_switching = True
-                break
-
-        assert reloaded_before_switching_finished
-        assert finished_switching
-
-        # Check if the reloaded config includes the new s3-node_exporter job:
-        prometheus.succeed(
-          """
-            curl -sf http://127.0.0.1:${toString queryPort}/api/v1/status/config \
-              | jq -r .data.yaml \
-              | yq '.scrape_configs | any(.job_name == "s3-node_exporter")' \
-              | grep true
-          """
-        )
   '';
 }
diff --git a/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-generated.nix b/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-generated.nix
index 8d25c1ec2c2f1..1ba0ea1519b06 100644
--- a/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-generated.nix
+++ b/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-generated.nix
@@ -4453,10 +4453,10 @@
       elpaBuild {
         pname = "org";
         ename = "org";
-        version = "9.7pre0.20240521.135840";
+        version = "9.7pre0.20240530.133120";
         src = fetchurl {
-          url = "https://elpa.gnu.org/devel/org-9.7pre0.20240521.135840.tar";
-          sha256 = "06j7kjbpd390i0kqvvnlkpdzqkwiicbdgjymif338l0qx2kl4sg5";
+          url = "https://elpa.gnu.org/devel/org-9.7pre0.20240530.133120.tar";
+          sha256 = "sha256-DuuLDBJKI2LwC0PH9PtujcPvaqaKLWYij+KzP1U7o9M=";
         };
         packageRequires = [ emacs ];
         meta = {
diff --git a/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-packages.nix b/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-packages.nix
index 00f50233d50f1..9550969adbbe2 100644
--- a/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-packages.nix
+++ b/pkgs/applications/editors/emacs/elisp-packages/elpa-devel-packages.nix
@@ -64,6 +64,26 @@ self: let
         '';
       });
 
+      org = super.org.overrideAttrs (old: {
+        dontUnpack = false;
+        patches = old.patches or [ ] ++ [
+          # security fix backported from 9.7.5
+          (pkgs.fetchpatch {
+            url = "https://git.savannah.gnu.org/cgit/emacs/org-mode.git/patch/?id=f4cc61636947b5c2f0afc67174dd369fe3277aa8";
+            hash = "sha256-bGgsnTSn6SMu1J8P2BfJjrKx2845FCsUB2okcIrEjDg=";
+            stripLen = 1;
+          })
+        ];
+        postPatch = old.postPatch or "" + "\n" + ''
+          pushd ..
+          local content_directory=${old.ename}-${old.version}
+          src=$PWD/$content_directory.tar
+          tar --create --verbose --file=$src $content_directory
+          popd
+        '';
+        dontBuild = true;
+      });
+
       pq = super.pq.overrideAttrs (old: {
         buildInputs = (old.buildInputs or [ ]) ++ [ pkgs.postgresql ];
       });
diff --git a/pkgs/applications/editors/emacs/elisp-packages/elpa-packages.nix b/pkgs/applications/editors/emacs/elisp-packages/elpa-packages.nix
index 18a0a36fc06cb..a3d729b636eee 100644
--- a/pkgs/applications/editors/emacs/elisp-packages/elpa-packages.nix
+++ b/pkgs/applications/editors/emacs/elisp-packages/elpa-packages.nix
@@ -141,6 +141,26 @@ self: let
         };
       });
 
+      org = super.org.overrideAttrs (old: {
+        dontUnpack = false;
+        patches = old.patches or [ ] ++ [
+          # security fix backported from 9.7.5
+          (pkgs.fetchpatch {
+            url = "https://git.savannah.gnu.org/cgit/emacs/org-mode.git/patch/?id=f4cc61636947b5c2f0afc67174dd369fe3277aa8";
+            hash = "sha256-bGgsnTSn6SMu1J8P2BfJjrKx2845FCsUB2okcIrEjDg=";
+            stripLen = 1;
+          })
+        ];
+        postPatch = old.postPatch or "" + "\n" + ''
+          pushd ..
+          local content_directory=${old.ename}-${old.version}
+          src=$PWD/$content_directory.tar
+          tar --create --verbose --file=$src $content_directory
+          popd
+        '';
+        dontBuild = true;
+      });
+
       plz = super.plz.overrideAttrs (
         old: {
           dontUnpack = false;
diff --git a/pkgs/applications/editors/emacs/sources.nix b/pkgs/applications/editors/emacs/sources.nix
index e1bea448e5856..da243044b689a 100644
--- a/pkgs/applications/editors/emacs/sources.nix
+++ b/pkgs/applications/editors/emacs/sources.nix
@@ -93,6 +93,12 @@ in
         url = "https://gitweb.gentoo.org/proj/emacs-patches.git/plain/emacs/28.2/12_all_org-remote-unsafe.patch?id=af40e12cb742510e5d40a06ffc6dfca97e340dd6";
         hash = "sha256-b6WU1o3PfDV/6BTPfPNUFny6oERJCNsDrvflxX3Yvek=";
       })
+
+      # security fix from Emacs 29.4
+      (fetchpatch {
+        url = "https://git.savannah.gnu.org/cgit/emacs.git/patch/?id=c645e1d8205f0f0663ec4a2d27575b238c646c7c";
+        hash = "sha256-G+gGQx5w3KuWMotR1n/sYYL8WyAABYW3fUPeffMMs38=";
+      })
     ];
   });
 
diff --git a/pkgs/build-support/dotnet/build-dotnet-module/hooks/dotnet-fixup-hook.sh b/pkgs/build-support/dotnet/build-dotnet-module/hooks/dotnet-fixup-hook.sh
index f9aba29a43555..a9701f2898b27 100644
--- a/pkgs/build-support/dotnet/build-dotnet-module/hooks/dotnet-fixup-hook.sh
+++ b/pkgs/build-support/dotnet/build-dotnet-module/hooks/dotnet-fixup-hook.sh
@@ -1,3 +1,12 @@
+# For compatibility, convert makeWrapperArgs to an array unless we are using
+# structured attributes. That is, we ensure that makeWrapperArgs is always an
+# array.
+# See https://github.com/NixOS/nixpkgs/blob/858f4db3048c5be3527e183470e93c1a72c5727c/pkgs/build-support/dotnet/build-dotnet-module/hooks/dotnet-fixup-hook.sh#L1-L3
+# and https://github.com/NixOS/nixpkgs/pull/313005#issuecomment-2175482920
+if [[ -z $__structuredAttrs ]]; then
+    makeWrapperArgs=( ${makeWrapperArgs-} )
+fi
+
 # First argument is the executable you want to wrap,
 # the second is the destination for the wrapper.
 wrapDotnetProgram() {
@@ -17,10 +26,8 @@ dotnetFromEnv'
 
     if [[ -n $__structuredAttrs ]]; then
         local -r dotnetRuntimeDepsArray=( "${dotnetRuntimeDeps[@]}" )
-        local -r makeWrapperArgsArray=( "${makeWrapperArgs[@]}" )
     else
         local -r dotnetRuntimeDepsArray=($dotnetRuntimeDeps)
-        local -r makeWrapperArgsArray=($makeWrapperArgs)
     fi
 
     local dotnetRuntimeDepsFlags=()
@@ -49,7 +56,7 @@ dotnetFromEnv'
         "${dotnetRuntimeDepsFlags[@]}" \
         "${dotnetRootFlagsArray[@]}" \
         "${gappsWrapperArgs[@]}" \
-        "${makeWrapperArgsArray[@]}"
+        "${makeWrapperArgs[@]}"
 
     echo "installed wrapper to "$2""
 }
diff --git a/pkgs/by-name/al/alertmanager-webhook-logger/package.nix b/pkgs/by-name/al/alertmanager-webhook-logger/package.nix
new file mode 100644
index 0000000000000..e04759dda7d37
--- /dev/null
+++ b/pkgs/by-name/al/alertmanager-webhook-logger/package.nix
@@ -0,0 +1,33 @@
+{ lib
+, stdenv
+, buildGoModule
+, fetchFromGitHub
+, nixosTests
+}:
+
+buildGoModule rec {
+  pname = "alertmanager-webhook-logger";
+  version = "1.0";
+  rev = "${version}";
+
+  src = fetchFromGitHub {
+    inherit rev;
+    owner = "tomtom-international";
+    repo = "alertmanager-webhook-logger";
+    hash = "sha256-mJbpDiTwUsFm0lDKz8UE/YF6sBvcSSR6WWLrfKvtri4=";
+  };
+
+  vendorHash = "sha256-gKtOoM9TuEIHgvSjZhqWmdexG2zDjlPuM0HjjP52DOI=";
+
+  doCheck = true;
+
+  passthru.tests = { inherit (nixosTests.prometheus) alertmanager; };
+
+  meta = with lib; {
+    description = "Generates (structured) log messages from Prometheus AlertManager webhook notifier";
+    mainProgram = "alertmanager-webhook-logger";
+    homepage = "https://github.com/tomtom-international/alertmanager-webhook-logger";
+    license = licenses.asl20;
+    maintainers = with maintainers; [ jpds ];
+  };
+}
diff --git a/pkgs/by-name/gp/gpsprune/package.nix b/pkgs/by-name/gp/gpsprune/package.nix
index 10975337b66e2..20b5dc15d6ce4 100644
--- a/pkgs/by-name/gp/gpsprune/package.nix
+++ b/pkgs/by-name/gp/gpsprune/package.nix
@@ -2,11 +2,11 @@
 
 stdenv.mkDerivation rec {
   pname = "gpsprune";
-  version = "24.1";
+  version = "24.2";
 
   src = fetchurl {
     url = "https://activityworkshop.net/software/gpsprune/gpsprune_${version}.jar";
-    sha256 = "sha256-k7aRuz/FOqQe2C0luiUwoFxnIvgM6opwsGZV7+hxJSM=";
+    sha256 = "sha256-wGg7WPj61yx7zMBIdH9ls18BnD1R713U5Vgc/kL9qYs=";
   };
 
   dontUnpack = true;
diff --git a/pkgs/by-name/mi/miru/darwin.nix b/pkgs/by-name/mi/miru/darwin.nix
new file mode 100644
index 0000000000000..af841bb43bbd6
--- /dev/null
+++ b/pkgs/by-name/mi/miru/darwin.nix
@@ -0,0 +1,33 @@
+{
+  stdenvNoCC,
+  fetchurl,
+  unzip,
+  makeWrapper,
+
+  pname,
+  version,
+  meta,
+}:
+stdenvNoCC.mkDerivation rec {
+  inherit pname version meta;
+
+  src = fetchurl {
+    url = "https://github.com/ThaUnknown/miru/releases/download/v${version}/mac-Miru-${version}-mac.zip";
+    hash = "sha256-OakGB5Fz1Tlxa/Uu7xHlKoEF9VRfWFQ9CjsR0eCRyQw=";
+  };
+
+  sourceRoot = ".";
+
+  nativeBuildInputs = [
+    unzip
+    makeWrapper
+  ];
+
+  installPhase = ''
+    runHook preInstall
+    mkdir -p $out/{bin,Applications}
+    cp -r Miru.app $out/Applications/
+    makeWrapper $out/Applications/Miru.app/Contents/MacOS/Miru $out/bin/miru
+    runHook postInstall
+  '';
+}
diff --git a/pkgs/by-name/mi/miru/linux.nix b/pkgs/by-name/mi/miru/linux.nix
new file mode 100644
index 0000000000000..f86aee30bfd1e
--- /dev/null
+++ b/pkgs/by-name/mi/miru/linux.nix
@@ -0,0 +1,31 @@
+{
+  fetchurl,
+  appimageTools,
+
+  pname,
+  version,
+  meta,
+}:
+
+appimageTools.wrapType2 rec {
+  inherit pname version meta;
+
+  src = fetchurl {
+    url = "https://github.com/ThaUnknown/miru/releases/download/v${version}/linux-Miru-${version}.AppImage";
+    name = "${pname}-${version}.AppImage";
+    sha256 = "sha256-aPutbJthUhZtBYkYuUB5v88OdhOrcnqw4AhnepfO1B4=";
+  };
+
+  extraInstallCommands =
+    let
+      contents = appimageTools.extractType2 { inherit pname version src; };
+    in
+    ''
+      mkdir -p "$out/share/applications"
+      mkdir -p "$out/share/lib/miru"
+      cp -r ${contents}/{locales,resources} "$out/share/lib/miru"
+      cp -r ${contents}/usr/* "$out"
+      cp "${contents}/${pname}.desktop" "$out/share/applications/"
+      substituteInPlace $out/share/applications/${pname}.desktop --replace 'Exec=AppRun' 'Exec=${pname}'
+    '';
+}
diff --git a/pkgs/by-name/mi/miru/package.nix b/pkgs/by-name/mi/miru/package.nix
index 75eb7b4823c5e..eeb1b0688dbc3 100644
--- a/pkgs/by-name/mi/miru/package.nix
+++ b/pkgs/by-name/mi/miru/package.nix
@@ -1,39 +1,22 @@
-{ lib
-, fetchurl
-, appimageTools
+{
+  stdenv,
+  lib,
+  callPackage,
 }:
-
-appimageTools.wrapType2 rec {
+let
   pname = "miru";
   version = "5.1.4";
-
-  src = fetchurl {
-    url = "https://github.com/ThaUnknown/miru/releases/download/v${version}/linux-Miru-${version}.AppImage";
-    name = "${pname}-${version}.AppImage";
-    sha256 = "sha256-aPutbJthUhZtBYkYuUB5v88OdhOrcnqw4AhnepfO1B4=";
-  };
-
-  extraInstallCommands =
-    let
-      contents = appimageTools.extractType2 { inherit pname version src; };
-    in
-    ''
-      mkdir -p "$out/share/applications"
-      mkdir -p "$out/share/lib/miru"
-      cp -r ${contents}/{locales,resources} "$out/share/lib/miru"
-      cp -r ${contents}/usr/* "$out"
-      cp "${contents}/${pname}.desktop" "$out/share/applications/"
-      substituteInPlace $out/share/applications/${pname}.desktop --replace 'Exec=AppRun' 'Exec=${pname}'
-    '';
-
   meta = with lib; {
     description = "Stream anime torrents, real-time with no waiting for downloads";
     homepage = "https://miru.watch";
     license = licenses.gpl3Plus;
-    maintainers = [ maintainers.d4ilyrun ];
+    maintainers = with maintainers; [
+      d4ilyrun
+      matteopacini
+    ];
     mainProgram = "miru";
 
-    platforms = [ "x86_64-linux" ];
+    platforms = [ "x86_64-linux" ] ++ platforms.darwin;
     sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
 
     longDescription = ''
@@ -49,4 +32,8 @@ appimageTools.wrapType2 rec {
       instead of flat out closing MPV.
     '';
   };
-}
+in
+if stdenv.isDarwin then
+  callPackage ./darwin.nix { inherit pname version meta; }
+else
+  callPackage ./linux.nix { inherit pname version meta; }
diff --git a/pkgs/by-name/so/solo5/package.nix b/pkgs/by-name/so/solo5/package.nix
index 57ccd1e24b2bf..1ca866c81e5e2 100644
--- a/pkgs/by-name/so/solo5/package.nix
+++ b/pkgs/by-name/so/solo5/package.nix
@@ -1,5 +1,5 @@
 { lib, stdenv, fetchurl, dosfstools, libseccomp, makeWrapper, mtools, parted
-, pkg-config, qemu, syslinux, util-linux }:
+, pkg-config, qemu_test, syslinux, util-linux }:
 
 let
   version = "0.8.1";
@@ -55,10 +55,12 @@ in stdenv.mkDerivation {
   '';
 
   doCheck = stdenv.hostPlatform.isLinux;
-  nativeCheckInputs = [ util-linux qemu ];
+  nativeCheckInputs = [ util-linux qemu_test ];
   checkPhase = ''
     runHook preCheck
     patchShebangs tests
+    substituteInPlace scripts/virtio-run/solo5-virtio-run.sh \
+      --replace " -no-acpi" ""
     ./tests/bats-core/bats ./tests/tests.bats
     runHook postCheck
   '';
diff --git a/pkgs/development/python-modules/textual/default.nix b/pkgs/development/python-modules/textual/default.nix
index c6a677f4c65a7..ea00817ab76de 100644
--- a/pkgs/development/python-modules/textual/default.nix
+++ b/pkgs/development/python-modules/textual/default.nix
@@ -17,7 +17,7 @@
 
 buildPythonPackage rec {
   pname = "textual";
-  version = "0.68.0";
+  version = "0.70.0";
   pyproject = true;
 
   disabled = pythonOlder "3.8";
@@ -26,7 +26,7 @@ buildPythonPackage rec {
     owner = "Textualize";
     repo = "textual";
     rev = "refs/tags/v${version}";
-    hash = "sha256-nxQVxe7lXMcxyqh4SWcQ/A6eQcEzkSshKmpweHpn7JE=";
+    hash = "sha256-nOgiMT+q2pc94Q6g3MvKQVhGJYSGNnoE+5/foK62zLM=";
   };
 
   build-system = [ poetry-core ];
diff --git a/pkgs/development/tools/language-servers/csharp-ls/default.nix b/pkgs/development/tools/language-servers/csharp-ls/default.nix
index eb8c6cd2bb31b..0972bac8c8486 100644
--- a/pkgs/development/tools/language-servers/csharp-ls/default.nix
+++ b/pkgs/development/tools/language-servers/csharp-ls/default.nix
@@ -8,9 +8,9 @@ in
 
 buildDotnetGlobalTool rec {
   pname = "csharp-ls";
-  version = "0.13.0";
+  version = "0.14.0";
 
-  nugetSha256 = "sha256-hhgMwDk3mT7E07REqZduTuEnS7D1tCgdxqN+MLNo9EI=";
+  nugetSha256 = "sha256-agcx7VPIqGhl3NzdGLPwXYJsRuvSjL4SdbNg9vFjIh4=";
 
   dotnet-sdk = sdk_8_0;
   dotnet-runtime = sdk_8_0;
diff --git a/pkgs/servers/home-assistant/custom-components/default.nix b/pkgs/servers/home-assistant/custom-components/default.nix
index d0323d2549890..3a1184016f3ef 100644
--- a/pkgs/servers/home-assistant/custom-components/default.nix
+++ b/pkgs/servers/home-assistant/custom-components/default.nix
@@ -48,6 +48,8 @@
 
   smartthinq-sensors = callPackage ./smartthinq-sensors {};
 
+  spook = callPackage ./spook {};
+
   tuya_local = callPackage ./tuya_local {};
 
   waste_collection_schedule = callPackage ./waste_collection_schedule {};
diff --git a/pkgs/servers/home-assistant/custom-components/spook/default.nix b/pkgs/servers/home-assistant/custom-components/spook/default.nix
new file mode 100644
index 0000000000000..280f7b9491b2a
--- /dev/null
+++ b/pkgs/servers/home-assistant/custom-components/spook/default.nix
@@ -0,0 +1,38 @@
+{
+  lib,
+  buildHomeAssistantComponent,
+  fetchFromGitHub,
+  pillow,
+  fnv-hash-fast,
+  psutil-home-assistant,
+  sqlalchemy,
+}:
+buildHomeAssistantComponent rec {
+  owner = "frenck";
+  domain = "spook";
+  version = "3.0.1";
+
+  src = fetchFromGitHub {
+    inherit owner;
+    repo = domain;
+    rev = "refs/tags/v${version}";
+    hash = "sha256-ChHsevryWuim8BEFqXVkCOW9fGMrt5vol+B2SreMUws=";
+  };
+
+  patches = [./remove-sub-integration-symlink-hack.patch];
+
+  dependencies = [
+    pillow
+    fnv-hash-fast
+    psutil-home-assistant
+    sqlalchemy
+  ];
+
+  meta = {
+    changelog = "https://github.com/frenck/spook/releases/tag/v${version}";
+    description = "Toolbox for Home Assistant";
+    homepage = "https://spook.boo/";
+    license = lib.licenses.mit;
+    maintainers = with lib.maintainers; [kkoniuszy];
+  };
+}
diff --git a/pkgs/servers/home-assistant/custom-components/spook/remove-sub-integration-symlink-hack.patch b/pkgs/servers/home-assistant/custom-components/spook/remove-sub-integration-symlink-hack.patch
new file mode 100644
index 0000000000000..4756e4a2c389a
--- /dev/null
+++ b/pkgs/servers/home-assistant/custom-components/spook/remove-sub-integration-symlink-hack.patch
@@ -0,0 +1,109 @@
+diff --git a/custom_components/spook/__init__.py b/custom_components/spook/__init__.py
+index 213fb2c..c7dc299 100644
+--- a/custom_components/spook/__init__.py
++++ b/custom_components/spook/__init__.py
+@@ -23,8 +23,6 @@ from .templating import SpookTemplateFunctionManager
+ from .util import (
+     async_ensure_template_environments_exists,
+     async_forward_setup_entry,
+-    link_sub_integrations,
+-    unlink_sub_integrations,
+ )
+
+ if TYPE_CHECKING:
+@@ -34,48 +32,6 @@ if TYPE_CHECKING:
+
+ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+     """Set up from a config entry."""
+-    # Symlink all sub integrations from Spook to the parent integrations folder
+-    # if one is missing, we have to restart Home Assistant.
+-    # This is a workaround for the fact that Home Assistant doesn't support
+-    # sub integrations.
+-    if await hass.async_add_executor_job(link_sub_integrations, hass):
+-        LOGGER.debug("Newly symlinked sub integrations, restarting Home Assistant")
+-
+-        @callback
+-        def _restart(_: Event | None = None) -> None:
+-            """Restart Home Assistant."""
+-            hass.data["homeassistant_stop"] = asyncio.create_task(
+-                hass.async_stop(RESTART_EXIT_CODE),
+-            )
+-
+-        # User asked to restart Home Assistant in the config flow.
+-        if hass.data.get(DOMAIN) == "Boo!":
+-            _restart()
+-            return False
+-
+-        # Should be OK to restart. Better to do it before anything else started.
+-        if hass.state == CoreState.starting:
+-            _restart()
+-            return False
+-
+-        # If all other fails, but we are not running yet... wait for it.
+-        if hass.state == CoreState.not_running:
+-            # Listen to both... just in case.
+-            hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _restart)
+-            hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _restart)
+-            return False
+-
+-        LOGGER.info(
+-            "Home Assistant needs to be restarted in for Spook to complete setting up",
+-        )
+-        ir.async_create_issue(
+-            hass=hass,
+-            domain=DOMAIN,
+-            issue_id="restart_required",
+-            is_fixable=True,
+-            severity=ir.IssueSeverity.WARNING,
+-            translation_key="restart_required",
+-        )
+
+     # Ensure template environments exists
+     async_ensure_template_environments_exists(hass)
+@@ -120,4 +76,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+
+ async def async_remove_entry(hass: HomeAssistant, _: ConfigEntry) -> None:
+     """Remove a config entry."""
+-    await hass.async_add_executor_job(unlink_sub_integrations, hass)
+diff --git a/custom_components/spook/util.py b/custom_components/spook/util.py
+index 32e9bd2..845d463 100644
+--- a/custom_components/spook/util.py
++++ b/custom_components/spook/util.py
+@@ -104,37 +104,6 @@ async def async_forward_platform_entry_setups_to_ectoplasm(
+     )
+
+
+-def link_sub_integrations(hass: HomeAssistant) -> bool:
+-    """Link Spook sub integrations."""
+-    LOGGER.debug("Linking up Spook sub integrations")
+-
+-    changes = False
+-    for manifest in Path(__file__).parent.rglob("integrations/*/manifest.json"):
+-        LOGGER.debug("Linking Spook sub integration: %s", manifest.parent.name)
+-        dest = Path(hass.config.config_dir) / "custom_components" / manifest.parent.name
+-        if not dest.exists():
+-            src = (
+-                Path(hass.config.config_dir)
+-                / "custom_components"
+-                / DOMAIN
+-                / "integrations"
+-                / manifest.parent.name
+-            )
+-            dest.symlink_to(src)
+-            changes = True
+-    return changes
+-
+-
+-def unlink_sub_integrations(hass: HomeAssistant) -> None:
+-    """Unlink Spook sub integrations."""
+-    LOGGER.debug("Unlinking Spook sub integrations")
+-    for manifest in Path(__file__).parent.rglob("integrations/*/manifest.json"):
+-        LOGGER.debug("Unlinking Spook sub integration: %s", manifest.parent.name)
+-        dest = Path(hass.config.config_dir) / "custom_components" / manifest.parent.name
+-        if dest.exists():
+-            dest.unlink()
+-
+-
+ @callback
+ def async_ensure_template_environments_exists(hass: HomeAssistant) -> None:
+     """Ensure default template environments exist.
diff --git a/pkgs/servers/janus-gateway/default.nix b/pkgs/servers/janus-gateway/default.nix
index 2fe617dea43b0..8a08962f646bb 100644
--- a/pkgs/servers/janus-gateway/default.nix
+++ b/pkgs/servers/janus-gateway/default.nix
@@ -15,13 +15,13 @@ in
 
 stdenv.mkDerivation rec {
   pname = "janus-gateway";
-  version = "1.2.2";
+  version = "1.2.3";
 
   src = fetchFromGitHub {
     owner = "meetecho";
     repo = pname;
     rev = "v${version}";
-    sha256 = "sha256-BS6ErS2Wi8pOy8oFmVnbujYPwClxX8e+GL4CcqvOL9E=";
+    sha256 = "sha256-3o9XxxTlWppq1mFgIUjstUFz6bT44mvBJa4FBgcc4Pc=";
   };
 
   nativeBuildInputs = [ autoreconfHook pkg-config gengetopt ];
diff --git a/pkgs/servers/monitoring/prometheus/alertmanager.nix b/pkgs/servers/monitoring/prometheus/alertmanager.nix
index ba1c35ee294d3..1db975f591d98 100644
--- a/pkgs/servers/monitoring/prometheus/alertmanager.nix
+++ b/pkgs/servers/monitoring/prometheus/alertmanager.nix
@@ -3,6 +3,7 @@
 , buildGoModule
 , fetchFromGitHub
 , installShellFiles
+, nixosTests
 }:
 
 buildGoModule rec {
@@ -39,6 +40,8 @@ buildGoModule rec {
     installShellCompletion amtool.zsh
   '';
 
+  passthru.tests = { inherit (nixosTests.prometheus) alertmanager; };
+
   meta = with lib; {
     description = "Alert dispatcher for the Prometheus monitoring system";
     homepage = "https://github.com/prometheus/alertmanager";
diff --git a/pkgs/servers/monitoring/prometheus/default.nix b/pkgs/servers/monitoring/prometheus/default.nix
index f7cc47311ef23..9229eb4546a2a 100644
--- a/pkgs/servers/monitoring/prometheus/default.nix
+++ b/pkgs/servers/monitoring/prometheus/default.nix
@@ -31,10 +31,10 @@
 }:
 
 let
-  version = "2.52.0";
+  version = "2.53.0";
   webUiStatic = fetchurl {
     url = "https://github.com/prometheus/prometheus/releases/download/v${version}/prometheus-web-ui-${version}.tar.gz";
-    hash = "sha256-/HTNDu/msUFLiqbD5ryVPlDOqfAT+8MEGNusmu3xH1w=";
+    hash = "sha256-oGhVpr47+blG7udu4Pcd+4Ndn0U+R47i69nheauDDoo=";
   };
 in
 buildGoModule rec {
@@ -47,10 +47,10 @@ buildGoModule rec {
     owner = "prometheus";
     repo = "prometheus";
     rev = "v${version}";
-    hash = "sha256-7HLSSls1aMZ6Zju2F9dArTFsCSESxDxWQ1a0QgD5Nxo=";
+    hash = "sha256-clI8/P6Gdl8WSK1DsXUj+M9f/h35GX961QtpyvGNxUY=";
   };
 
-  vendorHash = "sha256-IzYYfB8cvvQAsOqEREVaCe80V3hhhmOEYsl9E9NJq20=";
+  vendorHash = "sha256-0TR0jFEY+ls8C9EJEhSl2vX5VVZqZ8jGWDUalOddUPM=";
 
   excludedPackages = [ "documentation/prometheus-mixin" ];
 
diff --git a/pkgs/servers/monitoring/prometheus/pushgateway.nix b/pkgs/servers/monitoring/prometheus/pushgateway.nix
index c47019aeada72..cc8e11ade6355 100644
--- a/pkgs/servers/monitoring/prometheus/pushgateway.nix
+++ b/pkgs/servers/monitoring/prometheus/pushgateway.nix
@@ -1,4 +1,4 @@
-{ lib, buildGoModule, fetchFromGitHub, testers, prometheus-pushgateway }:
+{ lib, buildGoModule, fetchFromGitHub, nixosTests, testers, prometheus-pushgateway }:
 
 buildGoModule rec {
   pname = "pushgateway";
@@ -23,8 +23,11 @@ buildGoModule rec {
     "-X github.com/prometheus/common/version.BuildDate=19700101-00:00:00"
   ];
 
-  passthru.tests.version = testers.testVersion {
-    package = prometheus-pushgateway;
+  passthru.tests = {
+    inherit (nixosTests.prometheus) pushgateway;
+    version = testers.testVersion {
+      package = prometheus-pushgateway;
+    };
   };
 
   meta = with lib; {
diff --git a/pkgs/servers/monitoring/thanos/default.nix b/pkgs/servers/monitoring/thanos/default.nix
index d3e797a1c34c4..ce4a75cea75b7 100644
--- a/pkgs/servers/monitoring/thanos/default.nix
+++ b/pkgs/servers/monitoring/thanos/default.nix
@@ -37,7 +37,7 @@ buildGoModule rec {
   passthru = {
     updateScript = nix-update-script { };
     tests = {
-      inherit (nixosTests) prometheus;
+      inherit (nixosTests) thanos;
       version = testers.testVersion {
         command = "thanos --version";
         package = thanos;
diff --git a/pkgs/tools/misc/panoply/default.nix b/pkgs/tools/misc/panoply/default.nix
index d2a04e3c38f7e..2aef5a0788a8d 100644
--- a/pkgs/tools/misc/panoply/default.nix
+++ b/pkgs/tools/misc/panoply/default.nix
@@ -2,11 +2,11 @@
 
 stdenvNoCC.mkDerivation rec {
   pname = "panoply";
-  version = "5.4.1";
+  version = "5.4.3";
 
   src = fetchurl {
     url = "https://www.giss.nasa.gov/tools/panoply/download/PanoplyJ-${version}.tgz";
-    sha256 = "sha256-C/9kFWDpv4bzqpsUcGpZn7P+fqU6teU39HibYhhva3o=";
+    sha256 = "sha256-xey1Og0TEMgCc9Me/np7BGnUNYTz98gMFzLm2IcQHtw=";
   };
 
   nativeBuildInputs = [ makeWrapper ];