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