Commit c563d4a

HPCesia <me@hpcesia.com>
2026-07-01 14:07:28
new module(nixos): Forgejo Runner
In Forgejo v15, Forgejo Runner recommends it's new style settings, instead of Gitea Action Runner style settings.
1 parent 13fc298
Changed files (2)
nixos-modules/default.nix
@@ -1,4 +1,6 @@
 {
+  forgejo-runner = ./forgejo-runner.nix;
+
   mihomo = ./mihomo.nix;
 
   sub-store = ./sub-store.nix;
nixos-modules/forgejo-runner.nix
@@ -0,0 +1,277 @@
+{
+  inputs,
+  pkgs,
+  lib,
+  config,
+  ...
+}: let
+  cfg = config.services.forgejo-runner;
+  settingsFormat = pkgs.formats.yaml {};
+
+  # 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 = x: x.labels == [] || lib.any (label: lib.hasInfix ":docker:" label) x.labels;
+  hasDockerScheme = instance: _hasDockerScheme instance || lib.any _hasDockerScheme (lib.attrValues instance.servers);
+  wantsContainerRuntime = lib.any hasDockerScheme (lib.attrValues cfg.instances);
+
+  _hasHostScheme = x: lib.any (label: lib.hasSuffix ":host" label) x.labels;
+  hasHostScheme = instance: _hasHostScheme instance || lib.any _hasHostScheme (lib.attrValues instance.servers);
+
+  # provide shorthands for whether container runtimes are enabled
+  hasDocker = config.virtualisation.docker.enable;
+  hasPodman = config.virtualisation.podman.enable;
+
+  _tokenXorTokenFile = server:
+    (server.token == null && server.tokenFile != null)
+    || (server.token != null && server.tokenFile == null);
+  tokenXorTokenFile = instance: lib.any _tokenXorTokenFile (lib.attrValues instance.servers);
+
+  utils = pkgs.callPackage "${inputs.nixpkgs}/nixos/lib/utils.nix" {};
+in {
+  options.services.forgejo-runner = {
+    package = lib.mkPackageOption pkgs "forgejo-runner" {};
+
+    instances = lib.mkOption {
+      default = {};
+      description = ''
+        Forgejo Runner instances
+      '';
+      type = lib.types.attrsOf (lib.types.submodule ({
+        name,
+        config,
+        ...
+      }: {
+        options = {
+          enable = lib.mkEnableOption "Forgejo Runner instance";
+
+          name = lib.mkOption {
+            type = lib.types.str;
+            example = lib.literalExpression "config.networking.hostName";
+            default = name;
+            description = ''
+              The name identifying the runner instance towards the Gitea/Forgejo instance.
+            '';
+          };
+
+          servers = lib.mkOption {
+            type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
+              options = {
+                name = lib.mkOption {
+                  type = lib.types.str;
+                  example = "codeberg";
+                  default = name;
+                  description = ''
+                    The name identifying the runner instance towards the Gitea/Forgejo instance.
+                  '';
+                };
+
+                url = lib.mkOption {
+                  type = lib.types.str;
+                  example = "https://forge.example.com";
+                  description = ''
+                    Base URL of your Gitea/Forgejo instance.
+                  '';
+                };
+
+                uuid = lib.mkOption {
+                  type = lib.types.str;
+                  example = "c9e50be9-a7c3-4aee-ba35-624c4ff8c519";
+                  description = ''
+                    Base URL of your Gitea/Forgejo instance.
+                  '';
+                };
+
+                token = lib.mkOption {
+                  type = lib.types.nullOr lib.types.str;
+                  default = null;
+                  description = ''
+                    Plain token to register at the configured Gitea/Forgejo instance.
+                  '';
+                };
+
+                tokenFile = lib.mkOption {
+                  type = lib.types.nullOr (lib.types.either lib.types.str lib.types.path);
+                  default = null;
+                  description = ''
+                    Path to a file contains token to register at the configured Gitea/Forgejo instance.
+                  '';
+                };
+
+                labels = lib.mkOption {
+                  type = lib.types.listOf lib.types.str;
+                  example = lib.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 = ''
+                    Labels used to map jobs to their runtime environment for specific instance.
+
+                    Many common actions require bash, git and nodejs, as well as a filesystem
+                    that follows the filesystem hierarchy standard.
+                  '';
+                };
+              };
+            }));
+          };
+
+          labels = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            example = lib.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 = ''
+              Labels used to map jobs to their runtime environment.
+
+              Many common actions require bash, git and nodejs, as well as a filesystem
+              that follows the filesystem hierarchy standard.
+            '';
+          };
+
+          settings = lib.mkOption {
+            description = ''
+              Configuration for `act_runner daemon`.
+              See <https://gitea.com/gitea/act_runner/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration
+            '';
+
+            type = lib.types.submodule {
+              freeformType = settingsFormat.type;
+              options = {
+                runner.labels = lib.mkOption {
+                  type = lib.types.listOf lib.types.str;
+                  default = config.labels;
+                };
+                server.connections = lib.mkOption {
+                  type = lib.types.attrsOf (lib.types.submodule {
+                    freeformType = settingsFormat.type;
+                  });
+                  default =
+                    lib.mapAttrs (n: v: {
+                      inherit (v) url uuid labels;
+                      token = lib.mkIf (v.token != null) v.token;
+                      token_url = lib.mkIf (v.tokenFile != null) "file://${v.tokenFile}";
+                    })
+                    config.servers;
+                };
+              };
+            };
+          };
+
+          hostPackages = lib.mkOption {
+            type = lib.types.listOf lib.types.package;
+            default = with pkgs; [
+              bash
+              coreutils
+              curl
+              gawk
+              gitMinimal
+              gnused
+              nodejs
+              wget
+            ];
+            defaultText = lib.literalExpression ''
+              with pkgs; [
+                bash
+                coreutils
+                curl
+                gawk
+                gitMinimal
+                gnused
+                nodejs
+                wget
+              ]
+            '';
+            description = ''
+              List of packages, that are available to actions, when the runner is configured
+              with a host execution label.
+            '';
+          };
+        };
+      }));
+    };
+  };
+
+  config = lib.mkIf (cfg.instances != {}) {
+    assertions = [
+      {
+        assertion = lib.any tokenXorTokenFile (lib.attrValues cfg.instances);
+        message = "Servers of instances of forgejo-runner can have `token` or `tokenFile`, not both.";
+      }
+      {
+        assertion = wantsContainerRuntime -> hasDocker || hasPodman;
+        message = "Label configuration on forgejo-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;
+        configFile = settingsFormat.generate "config.yaml" instance.settings;
+      in
+        lib.nameValuePair "forgejo-runner-${utils.escapeSystemdPath name}" {
+          inherit (instance) enable;
+          description = "Forgejo Runner";
+          wants = ["network-online.target"];
+          after =
+            [
+              "network-online.target"
+            ]
+            ++ lib.optionals wantsDocker [
+              "docker.service"
+            ]
+            ++ lib.optionals wantsPodman [
+              "podman.service"
+            ];
+          wantedBy = [
+            "multi-user.target"
+          ];
+          environment =
+            lib.optionalAttrs wantsPodman {
+              DOCKER_HOST = "unix:///run/podman/podman.sock";
+            }
+            // {
+              HOME = "/var/lib/forgejo-runner/${name}";
+            };
+          path = with pkgs;
+            [
+              coreutils
+            ]
+            ++ lib.optionals wantsHost instance.hostPackages;
+          serviceConfig = {
+            DynamicUser = true;
+            StateDirectory = "forgejo-runner";
+            WorkingDirectory = "-/var/lib/forgejo-runner/${name}";
+
+            Restart = "on-failure";
+            RestartSec = 2;
+
+            ExecStart = "${cfg.package}/bin/act_runner daemon --config ${configFile}";
+            SupplementaryGroups =
+              lib.optionals wantsDocker [
+                "docker"
+              ]
+              ++ lib.optionals wantsPodman [
+                "podman"
+              ];
+          };
+        };
+    in
+      lib.mapAttrs' mkRunnerService cfg.instances;
+  };
+}