main
  1{
  2  pkgs,
  3  lib,
  4  config,
  5  ...
  6}: let
  7  cfg = config.services.forgejo-runner;
  8  settingsFormat = pkgs.formats.yaml {};
  9
 10  # Check whether any runner instance label requires a container runtime
 11  # Empty label strings result in the upstream defined defaultLabels, which require docker
 12  # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98
 13  _hasDockerScheme = x: x.labels == [] || lib.any (label: lib.hasInfix ":docker:" label) x.labels;
 14  hasDockerScheme = instance: _hasDockerScheme instance || lib.any _hasDockerScheme (lib.attrValues instance.servers);
 15  wantsContainerRuntime = lib.any hasDockerScheme (lib.attrValues cfg.instances);
 16
 17  _hasHostScheme = x: lib.any (label: lib.hasSuffix ":host" label) x.labels;
 18  hasHostScheme = instance: _hasHostScheme instance || lib.any _hasHostScheme (lib.attrValues instance.servers);
 19
 20  # provide shorthands for whether container runtimes are enabled
 21  hasDocker = config.virtualisation.docker.enable;
 22  hasPodman = config.virtualisation.podman.enable;
 23
 24  _tokenXorTokenFile = server:
 25    (server.token == null && server.tokenFile != null)
 26    || (server.token != null && server.tokenFile == null);
 27  tokenXorTokenFile = instance: lib.any _tokenXorTokenFile (lib.attrValues instance.servers);
 28
 29  utils = pkgs.callPackage "${pkgs.path}/nixos/lib/utils.nix" {};
 30in {
 31  options.services.forgejo-runner = {
 32    package = lib.mkPackageOption pkgs "forgejo-runner" {};
 33
 34    instances = lib.mkOption {
 35      default = {};
 36      description = ''
 37        Forgejo Runner instances
 38      '';
 39      type = lib.types.attrsOf (lib.types.submodule ({
 40        name,
 41        config,
 42        ...
 43      }: {
 44        options = {
 45          enable = lib.mkEnableOption "Forgejo Runner instance";
 46
 47          name = lib.mkOption {
 48            type = lib.types.str;
 49            example = lib.literalExpression "config.networking.hostName";
 50            default = name;
 51            description = ''
 52              The name identifying the runner instance towards the Gitea/Forgejo instance.
 53            '';
 54          };
 55
 56          servers = lib.mkOption {
 57            type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
 58              options = {
 59                name = lib.mkOption {
 60                  type = lib.types.str;
 61                  example = "codeberg";
 62                  default = name;
 63                  description = ''
 64                    The name identifying the runner instance towards the Gitea/Forgejo instance.
 65                  '';
 66                };
 67
 68                url = lib.mkOption {
 69                  type = lib.types.str;
 70                  example = "https://forge.example.com";
 71                  description = ''
 72                    Base URL of your Gitea/Forgejo instance.
 73                  '';
 74                };
 75
 76                uuid = lib.mkOption {
 77                  type = lib.types.str;
 78                  example = "c9e50be9-a7c3-4aee-ba35-624c4ff8c519";
 79                  description = ''
 80                    Base URL of your Gitea/Forgejo instance.
 81                  '';
 82                };
 83
 84                token = lib.mkOption {
 85                  type = lib.types.nullOr lib.types.str;
 86                  default = null;
 87                  description = ''
 88                    Plain token to register at the configured Gitea/Forgejo instance.
 89                  '';
 90                };
 91
 92                tokenFile = lib.mkOption {
 93                  type = lib.types.nullOr (lib.types.either lib.types.str lib.types.path);
 94                  default = null;
 95                  description = ''
 96                    Path to a file contains token to register at the configured Gitea/Forgejo instance.
 97                  '';
 98                };
 99
100                labels = lib.mkOption {
101                  type = lib.types.listOf lib.types.str;
102                  example = lib.literalExpression ''
103                    [
104                      # provide a debian base with nodejs for actions
105                      "debian-latest:docker://node:18-bullseye"
106                      # fake the ubuntu name, because node provides no ubuntu builds
107                      "ubuntu-latest:docker://node:18-bullseye"
108                      # provide native execution on the host
109                      #"native:host"
110                    ]
111                  '';
112                  description = ''
113                    Labels used to map jobs to their runtime environment for specific instance.
114
115                    Many common actions require bash, git and nodejs, as well as a filesystem
116                    that follows the filesystem hierarchy standard.
117                  '';
118                };
119              };
120            }));
121          };
122
123          labels = lib.mkOption {
124            type = lib.types.listOf lib.types.str;
125            example = lib.literalExpression ''
126              [
127                # provide a debian base with nodejs for actions
128                "debian-latest:docker://node:18-bullseye"
129                # fake the ubuntu name, because node provides no ubuntu builds
130                "ubuntu-latest:docker://node:18-bullseye"
131                # provide native execution on the host
132                #"native:host"
133              ]
134            '';
135            description = ''
136              Labels used to map jobs to their runtime environment.
137
138              Many common actions require bash, git and nodejs, as well as a filesystem
139              that follows the filesystem hierarchy standard.
140            '';
141          };
142
143          settings = lib.mkOption {
144            description = ''
145              Configuration for `act_runner daemon`.
146              See <https://gitea.com/gitea/act_runner/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration
147            '';
148
149            type = lib.types.submodule {
150              freeformType = settingsFormat.type;
151              options = {
152                runner.labels = lib.mkOption {
153                  type = lib.types.listOf lib.types.str;
154                  default = config.labels;
155                };
156                server.connections = lib.mkOption {
157                  type = lib.types.attrsOf (lib.types.submodule {
158                    freeformType = settingsFormat.type;
159                  });
160                  default =
161                    lib.mapAttrs (n: v: {
162                      inherit (v) url uuid labels;
163                      token = lib.mkIf (v.token != null) v.token;
164                      token_url = lib.mkIf (v.tokenFile != null) "file://${v.tokenFile}";
165                    })
166                    config.servers;
167                };
168              };
169            };
170          };
171
172          hostPackages = lib.mkOption {
173            type = lib.types.listOf lib.types.package;
174            default = with pkgs; [
175              bash
176              coreutils
177              curl
178              gawk
179              gitMinimal
180              gnused
181              nodejs
182              wget
183            ];
184            defaultText = lib.literalExpression ''
185              with pkgs; [
186                bash
187                coreutils
188                curl
189                gawk
190                gitMinimal
191                gnused
192                nodejs
193                wget
194              ]
195            '';
196            description = ''
197              List of packages, that are available to actions, when the runner is configured
198              with a host execution label.
199            '';
200          };
201        };
202      }));
203    };
204  };
205
206  config = lib.mkIf (cfg.instances != {}) {
207    assertions = [
208      {
209        assertion = lib.any tokenXorTokenFile (lib.attrValues cfg.instances);
210        message = "Servers of instances of forgejo-runner can have `token` or `tokenFile`, not both.";
211      }
212      {
213        assertion = wantsContainerRuntime -> hasDocker || hasPodman;
214        message = "Label configuration on forgejo-runner instance requires either docker or podman.";
215      }
216    ];
217
218    systemd.services = let
219      mkRunnerService = name: instance: let
220        wantsContainerRuntime = hasDockerScheme instance;
221        wantsHost = hasHostScheme instance;
222        wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable;
223        wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable;
224        configFile = settingsFormat.generate "config.yaml" instance.settings;
225      in
226        lib.nameValuePair "forgejo-runner-${utils.escapeSystemdPath name}" {
227          inherit (instance) enable;
228          description = "Forgejo Runner";
229          wants = ["network-online.target"];
230          after =
231            [
232              "network-online.target"
233            ]
234            ++ lib.optionals wantsDocker [
235              "docker.service"
236            ]
237            ++ lib.optionals wantsPodman [
238              "podman.service"
239            ];
240          wantedBy = [
241            "multi-user.target"
242          ];
243          environment =
244            lib.optionalAttrs wantsPodman {
245              DOCKER_HOST = "unix:///run/podman/podman.sock";
246            }
247            // {
248              HOME = "/var/lib/forgejo-runner/${name}";
249            };
250          path = with pkgs;
251            [
252              coreutils
253            ]
254            ++ lib.optionals wantsHost instance.hostPackages;
255          serviceConfig = {
256            DynamicUser = true;
257            StateDirectory = "forgejo-runner";
258            WorkingDirectory = "-/var/lib/forgejo-runner/${name}";
259
260            Restart = "on-failure";
261            RestartSec = 2;
262
263            ExecStart = "${cfg.package}/bin/act_runner daemon --config ${configFile}";
264            SupplementaryGroups =
265              lib.optionals wantsDocker [
266                "docker"
267              ]
268              ++ lib.optionals wantsPodman [
269                "podman"
270              ];
271          };
272        };
273    in
274      lib.mapAttrs' mkRunnerService cfg.instances;
275  };
276}