diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index 7a06c06fed1e..c0083eab5608 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -175,6 +175,11 @@ GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
There is a new module that provides doas, a lighter alternative to sudo with many of the same features.
+
+
+
+ Hercules CI Agent is a specialized build agent for projects built with Nix. See the options and setup.
+
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 08ed6e63e724..6f5eccf51212 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -263,6 +263,7 @@
./services/continuous-integration/buildbot/worker.nix
./services/continuous-integration/buildkite-agents.nix
./services/continuous-integration/hail.nix
+ ./services/continuous-integration/hercules-ci-agent/default.nix
./services/continuous-integration/hydra/default.nix
./services/continuous-integration/gitlab-runner.nix
./services/continuous-integration/gocd-agent/default.nix
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
new file mode 100644
index 000000000000..4aed493c0fb0
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
@@ -0,0 +1,213 @@
+/*
+
+This file is for options that NixOS and nix-darwin have in common.
+
+Platform-specific code is in the respective default.nix files.
+
+ */
+
+{ config, lib, options, pkgs, ... }:
+
+let
+ inherit (lib) mkOption mkIf types filterAttrs literalExample mkRenamedOptionModule;
+
+ cfg =
+ config.services.hercules-ci-agent;
+
+ format = pkgs.formats.toml {};
+
+ settingsModule = { config, ... }: {
+ freeformType = format.type;
+ options = {
+ baseDirectory = mkOption {
+ type = types.path;
+ default = "/var/lib/hercules-ci-agent";
+ description = ''
+ State directory (secrets, work directory, etc) for agent
+ '';
+ };
+ concurrentTasks = mkOption {
+ description = ''
+ Number of tasks to perform simultaneously, such as evaluations, derivations.
+
+ You must have a total capacity across agents of at least 2 concurrent tasks on x86_64-linux
+ to allow for import from derivation.
+ '';
+ type = types.int;
+ default = 4;
+ };
+ workDirectory = mkOption {
+ description = ''
+ The directory in which temporary subdirectories are created for task state. This includes sources for Nix evaluation.
+ '';
+ type = types.path;
+ default = config.baseDirectory + "/work";
+ defaultText = literalExample ''baseDirectory + "/work"'';
+ };
+ staticSecretsDirectory = mkOption {
+ description = ''
+ This is the default directory to look for statically configured secrets like cluster-join-token.key.
+ '';
+ type = types.path;
+ default = config.baseDirectory + "/secrets";
+ defaultText = literalExample ''baseDirectory + "/secrets"'';
+ };
+ clusterJoinTokenPath = mkOption {
+ description = ''
+ Location of the cluster-join-token.key file.
+ '';
+ type = types.path;
+ default = config.staticSecretsDirectory + "/cluster-join-token.key";
+ defaultText = literalExample ''staticSecretsDirectory + "/cluster-join-token.key"'';
+ # internal: It's a bit too detailed to show by default in the docs,
+ # but useful to define explicitly to allow reuse by other modules.
+ internal = true;
+ };
+ binaryCachesPath = mkOption {
+ description = ''
+ Location of the binary-caches.json file.
+ '';
+ type = types.path;
+ default = config.staticSecretsDirectory + "/binary-caches.json";
+ defaultText = literalExample ''staticSecretsDirectory + "/binary-caches.json"'';
+ # internal: It's a bit too detailed to show by default in the docs,
+ # but useful to define explicitly to allow reuse by other modules.
+ internal = true;
+ };
+ };
+ };
+
+ checkNix =
+ if !cfg.checkNix
+ then ""
+ else if lib.versionAtLeast config.nix.package.version "2.4.0"
+ then ""
+ else pkgs.stdenv.mkDerivation {
+ name = "hercules-ci-check-system-nix-src";
+ inherit (config.nix.package) src patches;
+ configurePhase = ":";
+ buildPhase = ''
+ echo "Checking in-memory pathInfoCache expiry"
+ if ! grep 'struct PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
+ cat 1>&2 <Hercules CI is a
+ continuous integation service that is centered around Nix.
+
+ Support is available at help@hercules-ci.com.
+ '';
+ };
+ patchNix = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Fix Nix 2.3 cache path metadata caching behavior. Has the effect of nix.package = patch pkgs.nix;
+
+ This option will be removed when Hercules CI Agent moves to Nix 2.4 (upcoming Nix release).
+ '';
+ };
+ checkNix = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to make sure that the system's Nix (nix-daemon) is compatible.
+
+ If you set this to false, please keep up with the change log.
+ '';
+ };
+ package = mkOption {
+ description = ''
+ Package containing the bin/hercules-ci-agent executable.
+ '';
+ type = types.package;
+ default = pkgs.hercules-ci-agent;
+ defaultText = literalExample "pkgs.hercules-ci-agent";
+ };
+ settings = mkOption {
+ description = ''
+ These settings are written to the agent.toml file.
+
+ Not all settings are listed as options, can be set nonetheless.
+
+ For the exhaustive list of settings, see .
+ '';
+ type = types.submoduleWith { modules = [ settingsModule ]; };
+ };
+
+ /*
+ Internal and/or computed values.
+
+ These are written as options instead of let binding to allow sharing with
+ default.nix on both NixOS and nix-darwin.
+ */
+ tomlFile = mkOption {
+ type = types.path;
+ internal = true;
+ defaultText = "generated hercules-ci-agent.toml";
+ description = ''
+ The fully assembled config file.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ nix.extraOptions = lib.addContextFrom checkNix ''
+ # A store path that was missing at first may well have finished building,
+ # even shortly after the previous lookup. This *also* applies to the daemon.
+ narinfo-cache-negative-ttl = 0
+ '';
+ nix.package = mkIf cfg.patchNix patchedNix;
+ services.hercules-ci-agent.tomlFile =
+ format.generate "hercules-ci-agent.toml" cfg.settings;
+ };
+}
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
new file mode 100644
index 000000000000..d2e7e8e18f94
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -0,0 +1,86 @@
+/*
+
+This file is for NixOS-specific options and configs.
+
+Code that is shared with nix-darwin goes in common.nix.
+
+ */
+
+{ pkgs, config, lib, ... }:
+
+let
+
+ inherit (lib) mkIf mkDefault;
+
+ cfg = config.services.hercules-ci-agent;
+
+ command = "${cfg.package}/bin/hercules-ci-agent --config ${cfg.tomlFile}";
+ testCommand = "${command} --test-configuration";
+
+in
+{
+ imports = [
+ ./common.nix
+ (lib.mkRenamedOptionModule ["services" "hercules-ci-agent" "user"] ["systemd" "services" "hercules-ci-agent" "serviceConfig" "User"])
+ ];
+
+ config = mkIf cfg.enable {
+
+ systemd.services.hercules-ci-agent = {
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network-online.target" ];
+ wants = [ "network-online.target" ];
+ path = [ config.nix.package ];
+ serviceConfig = {
+ User = "hercules-ci-agent";
+ ExecStart = command;
+ ExecStartPre = testCommand;
+ Restart = "on-failure";
+ RestartSec = 120;
+ StartLimitBurst = 30 * 1000000; # practically infinite
+ };
+ };
+
+ # Changes in the secrets do not affect the unit in any way that would cause
+ # a restart, which is currently necessary to reload the secrets.
+ systemd.paths.hercules-ci-agent-restart-files = {
+ wantedBy = [ "hercules-ci-agent.service" ];
+ pathConfig = {
+ Unit = "hercules-ci-agent-restarter.service";
+ PathChanged = [ cfg.settings.clusterJoinTokenPath cfg.settings.binaryCachesPath ];
+ };
+ };
+ systemd.services.hercules-ci-agent-restarter = {
+ serviceConfig.Type = "oneshot";
+ script = ''
+ # Wait a bit, with the effect of bundling up file changes into a single
+ # run of this script and hopefully a single restart.
+ sleep 10
+ if systemctl is-active --quiet hercules-ci-agent.service; then
+ if ${testCommand}; then
+ systemctl restart hercules-ci-agent.service
+ else
+ echo 1>&2 "WARNING: Not restarting agent because config is not valid at this time."
+ fi
+ else
+ echo 1>&2 "Not restarting hercules-ci-agent despite config file update, because it is not already active."
+ fi
+ '';
+ };
+
+ # Trusted user allows simplified configuration and better performance
+ # when operating in a cluster.
+ nix.trustedUsers = [ config.systemd.services.hercules-ci-agent.serviceConfig.User ];
+ services.hercules-ci-agent.settings.nixUserIsTrusted = true;
+
+ users.users.hercules-ci-agent = {
+ home = cfg.settings.baseDirectory;
+ createHome = true;
+ group = "hercules-ci-agent";
+ description = "Hercules CI Agent system user";
+ isSystemUser = true;
+ };
+
+ users.groups.hercules-ci-agent = {};
+ };
+}