diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index a9385ccb445c3..929c06fbb2159 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -132,6 +132,8 @@ See . - `hyphen` now supports over 40 language variants through `hyphenDicts` and now allows to enable all supported languages through `hyphenDicts.all`. +- The `services.forgejo-runner` module can now be used instead of `services.gitea-actions-runner`. It will set the `package` option to `pkgs.forgejo-runner` automatically, and use Forgejo's directories instead of Gitea's. No changes to the instance configurations are necessary. + - [services.resolved](#opt-services.resolved.enable) module was converted to RFC42-style settings. The moved options have also been renamed to match the upstream names. Aliases mean current configs will continue to function, but users should move to the new options as convenient. - Support for Bluetooth audio based on `bluez-alsa` has been added to the `hardware.alsa` module. It can be enabled with the new [enableBluetooth](#opt-hardware.alsa.enableBluetooth) option. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 4539b49f7ba82..287a70e33c1db 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -499,10 +499,11 @@ ./services/computing/slurm/slurm.nix ./services/computing/torque/mom.nix ./services/computing/torque/server.nix + ./services/continuous-integration/act-runner/forgejo-runner.nix + ./services/continuous-integration/act-runner/gitea-actions-runner.nix ./services/continuous-integration/buildbot/master.nix ./services/continuous-integration/buildbot/worker.nix ./services/continuous-integration/buildkite-agents.nix - ./services/continuous-integration/gitea-actions-runner.nix ./services/continuous-integration/github-runners.nix ./services/continuous-integration/gitlab-runner/runner.nix ./services/continuous-integration/gocd-agent/default.nix diff --git a/nixos/modules/services/continuous-integration/act-runner/forgejo-runner.nix b/nixos/modules/services/continuous-integration/act-runner/forgejo-runner.nix new file mode 100644 index 0000000000000..73b53fc9e022d --- /dev/null +++ b/nixos/modules/services/continuous-integration/act-runner/forgejo-runner.nix @@ -0,0 +1,14 @@ +{ + imports = [ + (import ./generic.nix { + attributeName = "forgejo-runner"; + name = "forgejo"; + prettyName = "Forgejo"; + runnerPrettyName = "Forgejo Runner"; + srcUrl = "https://code.forgejo.org/forgejo/runner"; + docsUrl = "https://forgejo.org/docs/latest/user/actions/overview"; + labelsUrl = "https://forgejo.org/docs/latest/admin/actions"; + mainProgram = "forgejo-runner"; + }) + ]; +} diff --git a/nixos/modules/services/continuous-integration/act-runner/generic.nix b/nixos/modules/services/continuous-integration/act-runner/generic.nix new file mode 100644 index 0000000000000..122a7023a9adb --- /dev/null +++ b/nixos/modules/services/continuous-integration/act-runner/generic.nix @@ -0,0 +1,315 @@ +{ + attributeName, + docsUrl, + labelsUrl, + mainProgram, + name, + prettyName, + runnerPrettyName, + srcUrl, +}: +{ + config, + lib, + pkgs, + utils, + ... +}: + +let + inherit (lib) + any + attrValues + concatStringsSep + escapeShellArg + getExe + hasInfix + hasSuffix + literalExpression + maintainers + mapAttrs' + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + nameValuePair + optionalAttrs + optionals + types + ; + + inherit (types) + attrsOf + either + listOf + nullOr + package + path + str + submodule + ; + + inherit (utils) + escapeSystemdPath + ; + + cfg = config.services.${attributeName}; + + settingsFormat = pkgs.formats.yaml { }; +in +{ + meta.maintainers = with maintainers; [ + hexa + sigmasquadron + ]; + + options.services.${attributeName} = { + package = mkPackageOption pkgs attributeName { }; + + instances = mkOption { + default = { }; + description = '' + ${runnerPrettyName} instances. + + See <${docsUrl}> for additional information on how to use these CI instances. + ''; + type = attrsOf (submodule { + options = { + enable = mkEnableOption "${runnerPrettyName} instance"; + + name = mkOption { + type = str; + example = literalExpression "config.networking.hostName"; + description = '' + The name identifying the runner instance towards the ${prettyName} instance. + ''; + }; + + url = mkOption { + type = str; + example = "https://forge.example.com"; + description = '' + Base URL of your ${prettyName} instance. + ''; + }; + + token = mkOption { + type = nullOr str; + default = null; + description = '' + Plain token to register at the configured ${prettyName} instance. + ''; + }; + + tokenFile = mkOption { + type = nullOr (either str path); + default = null; + description = '' + Path to an environment file, containing the `TOKEN` environment + variable, that holds a token to register at the configured + ${prettyName} instance. + ''; + }; + + labels = mkOption { + type = listOf str; + example = literalExpression '' + [ + # provide a debian base with nodejs for actions + "debian-latest:docker://node:18-bullseye" + # fake the ubuntu name, because node provides no ubuntu builds + "ubuntu-latest:docker://node:18-bullseye" + # provide native execution on the host + #"native:host" + ] + ''; + description = '' + Labels used to map jobs to their runtime environment. Changing these + labels currently requires a new registration token. + + Many common actions require `bash`, `git` and `nodejs`, as well as a + filesystem that follows the filesystem hierarchy standard. + + See <${labelsUrl}> for more information. + ''; + }; + settings = mkOption { + description = '' + Configuration for the `${mainProgram}` daemon. + See <${srcUrl}/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration. + ''; + + type = types.submodule { + freeformType = settingsFormat.type; + }; + + default = { }; + }; + + hostPackages = mkOption { + type = listOf package; + default = with pkgs; [ + bash + coreutils + curl + gawk + gitMinimal + gnused + nodejs + wget + ]; + defaultText = literalExpression '' + with pkgs; [ + bash + coreutils + curl + gawk + gitMinimal + gnused + nodejs + wget + ] + ''; + description = '' + List of packages that are available to actions when the runner + is configured with a host execution label. + ''; + }; + }; + }); + }; + }; + + config = mkMerge [ + (mkIf (cfg.instances != { }) ( + let + tokenXorTokenFile = + instance: + (instance.token == null && instance.tokenFile != null) + || (instance.token != null && instance.tokenFile == null); + + # Check whether any runner instance label requires a container runtime. + # Empty label strings result in the upstream defined defaultLabels, which require docker. + hasDockerScheme = + instance: instance.labels == [ ] || any (label: hasInfix ":docker:" label) instance.labels; + anyWantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances); + + # provide shorthands for whether container runtimes are enabled and whether host execution is possible. + hasDocker = config.virtualisation.docker.enable; + hasPodman = config.virtualisation.podman.enable; + hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels; + in + { + assertions = [ + { + assertion = any tokenXorTokenFile (attrValues cfg.instances); + message = "${runnerPrettyName} instances may have either a `token` or a `tokenFile` configured, but not both simultaneously."; + } + { + assertion = anyWantsContainerRuntime -> hasDocker || hasPodman; + message = "At least one of the configured ${runnerPrettyName} instances require a container hypervisor, but neither Docker nor Podman are enabled."; + } + ]; + + systemd.services = + let + mkRunnerService = + id: instance: + let + wantsContainerRuntime = hasDockerScheme instance; + wantsHost = hasHostScheme instance; + wantsDocker = wantsContainerRuntime && hasDocker; + wantsPodman = wantsContainerRuntime && hasPodman; + configFile = settingsFormat.generate "config.yaml" instance.settings; + in + nameValuePair "${name}-runner-${escapeSystemdPath id}" { + inherit (instance) enable; + description = runnerPrettyName; + wants = [ "network-online.target" ]; + after = [ + "network-online.target" + ] + ++ optionals wantsDocker [ + "docker.service" + ] + ++ optionals wantsPodman [ + "podman.service" + ]; + wantedBy = [ + "multi-user.target" + ]; + environment = + optionalAttrs (instance.token != null) { + TOKEN = "${instance.token}"; + } + // optionalAttrs wantsPodman { + DOCKER_HOST = "unix:///run/podman/podman.sock"; + } + // { + HOME = "/var/lib/${name}-runner/${id}"; + }; + path = + with pkgs; + [ + coreutils + ] + ++ optionals wantsHost instance.hostPackages; + serviceConfig = { + DynamicUser = true; + User = "${name}-runner"; + StateDirectory = "${name}-runner"; + WorkingDirectory = "-/var/lib/${name}-runner/${id}"; + + # act_runner might fail when the forge is restarted during an upgrade. + Restart = "on-failure"; + RestartSec = 2; + + ExecStartPre = [ + (pkgs.writeShellScript "${name}-register-runner-${id}" '' + export INSTANCE_DIR="$STATE_DIRECTORY/${id}" + mkdir -vp "$INSTANCE_DIR" + cd "$INSTANCE_DIR" + + # force reregistration on changed labels + export LABELS_FILE="$INSTANCE_DIR/.labels" + export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)" + export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)" + + if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then + # remove existing registration file, so that changing the labels forces a re-registration + rm -v "$INSTANCE_DIR/.runner" || true + + # perform the registration + ${getExe cfg.package} register --no-interactive \ + --instance ${escapeShellArg instance.url} \ + --token "$TOKEN" \ + --name ${escapeShellArg instance.name} \ + --labels ${escapeShellArg (concatStringsSep "," instance.labels)} \ + --config ${configFile} + + # and write back the configured labels + echo "$LABELS_WANTED" > "$LABELS_FILE" + fi + + '') + ]; + ExecStart = "${getExe cfg.package} daemon --config ${configFile}"; + SupplementaryGroups = + optionals wantsDocker [ + "docker" + ] + ++ optionals wantsPodman [ + "podman" + ]; + } + // optionalAttrs (instance.tokenFile != null) { + EnvironmentFile = instance.tokenFile; + }; + }; + in + mapAttrs' mkRunnerService cfg.instances; + } + )) + ]; +} diff --git a/nixos/modules/services/continuous-integration/act-runner/gitea-actions-runner.nix b/nixos/modules/services/continuous-integration/act-runner/gitea-actions-runner.nix new file mode 100644 index 0000000000000..424482a61d05a --- /dev/null +++ b/nixos/modules/services/continuous-integration/act-runner/gitea-actions-runner.nix @@ -0,0 +1,14 @@ +{ + imports = [ + (import ./generic.nix { + attributeName = "gitea-actions-runner"; + docsUrl = "https://docs.gitea.com/usage/actions"; + labelsUrl = "https://docs.gitea.com/usage/actions/act-runner#labels"; + mainProgram = "act_runner"; + name = "gitea"; + prettyName = "Gitea"; + runnerPrettyName = "Gitea Actions Runner"; + srcUrl = "https://gitea.com/gitea/act_runner"; + }) + ]; +} diff --git a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix deleted file mode 100644 index 3f7643a941d06..0000000000000 --- a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix +++ /dev/null @@ -1,283 +0,0 @@ -{ - config, - lib, - pkgs, - utils, - ... -}: - -let - inherit (lib) - any - attrValues - concatStringsSep - escapeShellArg - hasInfix - hasSuffix - optionalAttrs - optionals - literalExpression - mapAttrs' - mkEnableOption - mkOption - mkPackageOption - mkIf - nameValuePair - types - ; - - inherit (utils) - escapeSystemdPath - ; - - cfg = config.services.gitea-actions-runner; - - settingsFormat = pkgs.formats.yaml { }; - - # Check whether any runner instance label requires a container runtime - # Empty label strings result in the upstream defined defaultLabels, which require docker - # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98 - hasDockerScheme = - instance: instance.labels == [ ] || any (label: hasInfix ":docker:" label) instance.labels; - wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances); - - hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels; - - # provide shorthands for whether container runtimes are enabled - hasDocker = config.virtualisation.docker.enable; - hasPodman = config.virtualisation.podman.enable; - - tokenXorTokenFile = - instance: - (instance.token == null && instance.tokenFile != null) - || (instance.token != null && instance.tokenFile == null); -in -{ - meta.maintainers = with lib.maintainers; [ - hexa - ]; - - options.services.gitea-actions-runner = with types; { - package = mkPackageOption pkgs "gitea-actions-runner" { }; - - instances = mkOption { - default = { }; - description = '' - Gitea Actions Runner instances. - ''; - type = attrsOf (submodule { - options = { - enable = mkEnableOption "Gitea Actions Runner instance"; - - name = mkOption { - type = str; - example = literalExpression "config.networking.hostName"; - description = '' - The name identifying the runner instance towards the Gitea/Forgejo instance. - ''; - }; - - url = mkOption { - type = str; - example = "https://forge.example.com"; - description = '' - Base URL of your Gitea/Forgejo instance. - ''; - }; - - token = mkOption { - type = nullOr str; - default = null; - description = '' - Plain token to register at the configured Gitea/Forgejo instance. - ''; - }; - - tokenFile = mkOption { - type = nullOr (either str path); - default = null; - description = '' - Path to an environment file, containing the `TOKEN` environment - variable, that holds a token to register at the configured - Gitea/Forgejo instance. - ''; - }; - - labels = mkOption { - type = listOf str; - example = literalExpression '' - [ - # provide a debian base with nodejs for actions - "debian-latest:docker://node:18-bullseye" - # fake the ubuntu name, because node provides no ubuntu builds - "ubuntu-latest:docker://node:18-bullseye" - # provide native execution on the host - #"native:host" - ] - ''; - description = '' - Labels used to map jobs to their runtime environment. Changing these - labels currently requires a new registration token. - - Many common actions require bash, git and nodejs, as well as a filesystem - that follows the filesystem hierarchy standard. - ''; - }; - settings = mkOption { - description = '' - Configuration for `act_runner daemon`. - See for an example configuration - ''; - - type = types.submodule { - freeformType = settingsFormat.type; - }; - - default = { }; - }; - - hostPackages = mkOption { - type = listOf package; - default = with pkgs; [ - bash - coreutils - curl - gawk - gitMinimal - gnused - nodejs - wget - ]; - defaultText = literalExpression '' - with pkgs; [ - bash - coreutils - curl - gawk - gitMinimal - gnused - nodejs - wget - ] - ''; - description = '' - List of packages, that are available to actions, when the runner is configured - with a host execution label. - ''; - }; - }; - }); - }; - }; - - config = mkIf (cfg.instances != { }) { - assertions = [ - { - assertion = any tokenXorTokenFile (attrValues cfg.instances); - message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both."; - } - { - assertion = wantsContainerRuntime -> hasDocker || hasPodman; - message = "Label configuration on gitea-actions-runner instance requires either docker or podman."; - } - ]; - - systemd.services = - let - mkRunnerService = - name: instance: - let - wantsContainerRuntime = hasDockerScheme instance; - wantsHost = hasHostScheme instance; - wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable; - wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable; - configFile = settingsFormat.generate "config.yaml" instance.settings; - in - nameValuePair "gitea-runner-${escapeSystemdPath name}" { - inherit (instance) enable; - description = "Gitea Actions Runner"; - wants = [ "network-online.target" ]; - after = [ - "network-online.target" - ] - ++ optionals wantsDocker [ - "docker.service" - ] - ++ optionals wantsPodman [ - "podman.service" - ]; - wantedBy = [ - "multi-user.target" - ]; - environment = - optionalAttrs (instance.token != null) { - TOKEN = "${instance.token}"; - } - // optionalAttrs wantsPodman { - DOCKER_HOST = "unix:///run/podman/podman.sock"; - } - // { - HOME = "/var/lib/gitea-runner/${name}"; - }; - path = - with pkgs; - [ - coreutils - ] - ++ lib.optionals wantsHost instance.hostPackages; - serviceConfig = { - DynamicUser = true; - User = "gitea-runner"; - StateDirectory = "gitea-runner"; - WorkingDirectory = "-/var/lib/gitea-runner/${name}"; - - # gitea-runner might fail when gitea is restarted during upgrade. - Restart = "on-failure"; - RestartSec = 2; - - ExecStartPre = [ - (pkgs.writeShellScript "gitea-register-runner-${name}" '' - export INSTANCE_DIR="$STATE_DIRECTORY/${name}" - mkdir -vp "$INSTANCE_DIR" - cd "$INSTANCE_DIR" - - # force reregistration on changed labels - export LABELS_FILE="$INSTANCE_DIR/.labels" - export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)" - export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)" - - if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then - # remove existing registration file, so that changing the labels forces a re-registration - rm -v "$INSTANCE_DIR/.runner" || true - - # perform the registration - ${cfg.package}/bin/act_runner register --no-interactive \ - --instance ${escapeShellArg instance.url} \ - --token "$TOKEN" \ - --name ${escapeShellArg instance.name} \ - --labels ${escapeShellArg (concatStringsSep "," instance.labels)} \ - --config ${configFile} - - # and write back the configured labels - echo "$LABELS_WANTED" > "$LABELS_FILE" - fi - - '') - ]; - ExecStart = "${cfg.package}/bin/act_runner daemon --config ${configFile}"; - SupplementaryGroups = - optionals wantsDocker [ - "docker" - ] - ++ optionals wantsPodman [ - "podman" - ]; - } - // optionalAttrs (instance.tokenFile != null) { - EnvironmentFile = instance.tokenFile; - }; - }; - in - mapAttrs' mkRunnerService cfg.instances; - }; -} diff --git a/nixos/tests/forgejo.nix b/nixos/tests/forgejo.nix index 3574b93edec5f..85221a33c9ee3 100644 --- a/nixos/tests/forgejo.nix +++ b/nixos/tests/forgejo.nix @@ -62,18 +62,15 @@ let specialisation.runner = { inheritParentConfig = true; - configuration.services.gitea-actions-runner = { - package = pkgs.forgejo-runner; - instances."test" = { - enable = true; - name = "ci"; - url = "http://localhost:3000"; - labels = [ - # type ":host" does not depend on docker/podman/lxc - "native:host" - ]; - tokenFile = "/var/lib/forgejo/runner_token"; - }; + configuration.services.forgejo-runner.instances."test" = { + enable = true; + name = "ci"; + url = "http://localhost:3000"; + labels = [ + # type ":host" does not depend on docker/podman/lxc + "native:host" + ]; + tokenFile = "/var/lib/forgejo/runner_token"; }; }; specialisation.dump = { @@ -222,8 +219,8 @@ let "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo forgejo actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token" ) server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test") - server.wait_for_unit("gitea-runner-test.service") - server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'") + server.wait_for_unit("forgejo-runner-test.service") + server.succeed("journalctl -o cat -u forgejo-runner-test.service | grep -q 'Runner registered successfully'") # enable actions feature for this repository, defaults to disabled server.succeed(