{ config , lib , pkgs , utils , ... }: let inherit (lib) any attrValues concatStringsSep escapeShellArg hasInfix hasSuffix optionalAttrs optionals literalExpression mapAttrs' mkEnableOption mkOption mkPackageOptionMD mkIf nameValuePair types ; inherit (utils) escapeSystemdPath ; cfg = config.services.gitea-actions-runner; # Check whether any runner instance label requires a container runtime # Empty label strings result in the upstream defined defaultLabels, which require docker # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98 hasDockerScheme = instance: instance.labels == [] || any (label: hasInfix ":docker:" label) instance.labels; wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances); hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels; # provide shorthands for whether container runtimes are enabled hasDocker = config.virtualisation.docker.enable; hasPodman = config.virtualisation.podman.enable; tokenXorTokenFile = instance: (instance.token == null && instance.tokenFile != null) || (instance.token != null && instance.tokenFile == null); in { meta.maintainers = with lib.maintainers; [ hexa ]; options.services.gitea-actions-runner = with types; { package = mkPackageOptionMD pkgs "gitea-actions-runner" { }; instances = mkOption { default = {}; description = lib.mdDoc '' Gitea Actions Runner instances. ''; type = attrsOf (submodule { options = { enable = mkEnableOption (lib.mdDoc "Gitea Actions Runner instance"); name = mkOption { type = str; example = literalExpression "config.networking.hostName"; description = lib.mdDoc '' The name identifying the runner instance towards the Gitea/Forgejo instance. ''; }; url = mkOption { type = str; example = "https://forge.example.com"; description = lib.mdDoc '' Base URL of your Gitea/Forgejo instance. ''; }; token = mkOption { type = nullOr str; default = null; description = lib.mdDoc '' Plain token to register at the configured Gitea/Forgejo instance. ''; }; tokenFile = mkOption { type = nullOr (either str path); default = null; description = lib.mdDoc '' Path to an environment file, containing the `TOKEN` environment variable, that holds a token to register at the configured Gitea/Forgejo instance. ''; }; labels = mkOption { type = listOf str; example = literalExpression '' [ # provide a debian base with nodejs for actions "debian-latest:docker://node:18-bullseye" # fake the ubuntu name, because node provides no ubuntu builds "ubuntu-latest:docker://node:18-bullseye" # provide native execution on the host #"native:host" ] ''; description = lib.mdDoc '' Labels used to map jobs to their runtime environment. Changing these labels currently requires a new registration token. Many common actions require bash, git and nodejs, as well as a filesystem that follows the filesystem hierarchy standard. ''; }; hostPackages = mkOption { type = listOf package; default = with pkgs; [ bash coreutils curl gawk gitMinimal gnused nodejs wget ]; defaultText = literalExpression '' with pkgs; [ bash coreutils curl gawk gitMinimal gnused nodejs wget ] ''; description = lib.mdDoc '' List of packages, that are available to actions, when the runner is configured with a host execution label. ''; }; }; }); }; }; config = mkIf (cfg.instances != {}) { assertions = [ { assertion = any tokenXorTokenFile (attrValues cfg.instances); message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both."; } { assertion = wantsContainerRuntime -> hasDocker || hasPodman; message = "Label configuration on gitea-actions-runner instance requires either docker or podman."; } ]; systemd.services = let mkRunnerService = name: instance: let wantsContainerRuntime = hasDockerScheme instance; wantsHost = hasHostScheme instance; wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable; wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable; in nameValuePair "gitea-runner-${escapeSystemdPath name}" { inherit (instance) enable; description = "Gitea Actions Runner"; after = [ "network-online.target" ] ++ optionals (wantsDocker) [ "docker.service" ] ++ optionals (wantsPodman) [ "podman.service" ]; wantedBy = [ "multi-user.target" ]; environment = optionalAttrs (instance.token != null) { TOKEN = "${instance.token}"; } // optionalAttrs (wantsPodman) { DOCKER_HOST = "unix:///run/podman/podman.sock"; }; path = with pkgs; [ coreutils ] ++ lib.optionals wantsHost instance.hostPackages; serviceConfig = { DynamicUser = true; User = "gitea-runner"; StateDirectory = "gitea-runner"; WorkingDirectory = "-/var/lib/gitea-runner/${name}"; ExecStartPre = pkgs.writeShellScript "gitea-register-runner-${name}" '' export INSTANCE_DIR="$STATE_DIRECTORY/${name}" mkdir -vp "$INSTANCE_DIR" cd "$INSTANCE_DIR" # force reregistration on changed labels export LABELS_FILE="$INSTANCE_DIR/.labels" export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)" export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)" if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then # remove existing registration file, so that changing the labels forces a re-registration rm -v "$INSTANCE_DIR/.runner" || true # perform the registration ${cfg.package}/bin/act_runner register --no-interactive \ --instance ${escapeShellArg instance.url} \ --token "$TOKEN" \ --name ${escapeShellArg instance.name} \ --labels ${escapeShellArg (concatStringsSep "," instance.labels)} # and write back the configured labels echo "$LABELS_WANTED" > "$LABELS_FILE" fi ''; ExecStart = "${cfg.package}/bin/act_runner daemon"; SupplementaryGroups = optionals (wantsDocker) [ "docker" ] ++ optionals (wantsPodman) [ "podman" ]; } // optionalAttrs (instance.tokenFile != null) { EnvironmentFile = instance.tokenFile; }; }; in mapAttrs' mkRunnerService cfg.instances; }; }