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}