1{ config
2, lib
3, pkgs
4, utils
5, ...
6}:
7
8let
9 inherit (lib)
10 any
11 attrValues
12 concatStringsSep
13 escapeShellArg
14 hasInfix
15 hasSuffix
16 optionalAttrs
17 optionals
18 literalExpression
19 mapAttrs'
20 mkEnableOption
21 mkOption
22 mkPackageOptionMD
23 mkIf
24 nameValuePair
25 types
26 ;
27
28 inherit (utils)
29 escapeSystemdPath
30 ;
31
32 cfg = config.services.gitea-actions-runner;
33
34 # Check whether any runner instance label requires a container runtime
35 # Empty label strings result in the upstream defined defaultLabels, which require docker
36 # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98
37 hasDockerScheme = instance:
38 instance.labels == [] || any (label: hasInfix ":docker:" label) instance.labels;
39 wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances);
40
41 hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels;
42
43 # provide shorthands for whether container runtimes are enabled
44 hasDocker = config.virtualisation.docker.enable;
45 hasPodman = config.virtualisation.podman.enable;
46
47 tokenXorTokenFile = instance:
48 (instance.token == null && instance.tokenFile != null) ||
49 (instance.token != null && instance.tokenFile == null);
50in
51{
52 meta.maintainers = with lib.maintainers; [
53 hexa
54 ];
55
56 options.services.gitea-actions-runner = with types; {
57 package = mkPackageOptionMD pkgs "gitea-actions-runner" { };
58
59 instances = mkOption {
60 default = {};
61 description = lib.mdDoc ''
62 Gitea Actions Runner instances.
63 '';
64 type = attrsOf (submodule {
65 options = {
66 enable = mkEnableOption (lib.mdDoc "Gitea Actions Runner instance");
67
68 name = mkOption {
69 type = str;
70 example = literalExpression "config.networking.hostName";
71 description = lib.mdDoc ''
72 The name identifying the runner instance towards the Gitea/Forgejo instance.
73 '';
74 };
75
76 url = mkOption {
77 type = str;
78 example = "https://forge.example.com";
79 description = lib.mdDoc ''
80 Base URL of your Gitea/Forgejo instance.
81 '';
82 };
83
84 token = mkOption {
85 type = nullOr str;
86 default = null;
87 description = lib.mdDoc ''
88 Plain token to register at the configured Gitea/Forgejo instance.
89 '';
90 };
91
92 tokenFile = mkOption {
93 type = nullOr (either str path);
94 default = null;
95 description = lib.mdDoc ''
96 Path to an environment file, containing the `TOKEN` environment
97 variable, that holds a token to register at the configured
98 Gitea/Forgejo instance.
99 '';
100 };
101
102 labels = mkOption {
103 type = listOf str;
104 example = literalExpression ''
105 [
106 # provide a debian base with nodejs for actions
107 "debian-latest:docker://node:18-bullseye"
108 # fake the ubuntu name, because node provides no ubuntu builds
109 "ubuntu-latest:docker://node:18-bullseye"
110 # provide native execution on the host
111 #"native:host"
112 ]
113 '';
114 description = lib.mdDoc ''
115 Labels used to map jobs to their runtime environment. Changing these
116 labels currently requires a new registration token.
117
118 Many common actions require bash, git and nodejs, as well as a filesystem
119 that follows the filesystem hierarchy standard.
120 '';
121 };
122
123 hostPackages = mkOption {
124 type = listOf package;
125 default = with pkgs; [
126 bash
127 coreutils
128 curl
129 gawk
130 gitMinimal
131 gnused
132 nodejs
133 wget
134 ];
135 defaultText = literalExpression ''
136 with pkgs; [
137 bash
138 coreutils
139 curl
140 gawk
141 gitMinimal
142 gnused
143 nodejs
144 wget
145 ]
146 '';
147 description = lib.mdDoc ''
148 List of packages, that are available to actions, when the runner is configured
149 with a host execution label.
150 '';
151 };
152 };
153 });
154 };
155 };
156
157 config = mkIf (cfg.instances != {}) {
158 assertions = [ {
159 assertion = any tokenXorTokenFile (attrValues cfg.instances);
160 message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both.";
161 } {
162 assertion = wantsContainerRuntime -> hasDocker || hasPodman;
163 message = "Label configuration on gitea-actions-runner instance requires either docker or podman.";
164 } ];
165
166 systemd.services = let
167 mkRunnerService = name: instance: let
168 wantsContainerRuntime = hasDockerScheme instance;
169 wantsHost = hasHostScheme instance;
170 wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable;
171 wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable;
172 in
173 nameValuePair "gitea-runner-${escapeSystemdPath name}" {
174 inherit (instance) enable;
175 description = "Gitea Actions Runner";
176 after = [
177 "network-online.target"
178 ] ++ optionals (wantsDocker) [
179 "docker.service"
180 ] ++ optionals (wantsPodman) [
181 "podman.service"
182 ];
183 wantedBy = [
184 "multi-user.target"
185 ];
186 environment = optionalAttrs (instance.token != null) {
187 TOKEN = "${instance.token}";
188 } // optionalAttrs (wantsPodman) {
189 DOCKER_HOST = "unix:///run/podman/podman.sock";
190 };
191 path = with pkgs; [
192 coreutils
193 ] ++ lib.optionals wantsHost instance.hostPackages;
194 serviceConfig = {
195 DynamicUser = true;
196 User = "gitea-runner";
197 StateDirectory = "gitea-runner";
198 WorkingDirectory = "-/var/lib/gitea-runner/${name}";
199 ExecStartPre = pkgs.writeShellScript "gitea-register-runner-${name}" ''
200 export INSTANCE_DIR="$STATE_DIRECTORY/${name}"
201 mkdir -vp "$INSTANCE_DIR"
202 cd "$INSTANCE_DIR"
203
204 # force reregistration on changed labels
205 export LABELS_FILE="$INSTANCE_DIR/.labels"
206 export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)"
207 export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)"
208
209 if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then
210 # remove existing registration file, so that changing the labels forces a re-registration
211 rm -v "$INSTANCE_DIR/.runner" || true
212
213 # perform the registration
214 ${cfg.package}/bin/act_runner register --no-interactive \
215 --instance ${escapeShellArg instance.url} \
216 --token "$TOKEN" \
217 --name ${escapeShellArg instance.name} \
218 --labels ${escapeShellArg (concatStringsSep "," instance.labels)}
219
220 # and write back the configured labels
221 echo "$LABELS_WANTED" > "$LABELS_FILE"
222 fi
223
224 '';
225 ExecStart = "${cfg.package}/bin/act_runner daemon";
226 SupplementaryGroups = optionals (wantsDocker) [
227 "docker"
228 ] ++ optionals (wantsPodman) [
229 "podman"
230 ];
231 } // optionalAttrs (instance.tokenFile != null) {
232 EnvironmentFile = instance.tokenFile;
233 };
234 };
235 in mapAttrs' mkRunnerService cfg.instances;
236 };
237}