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}