Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b7fbc38
nixos/gitea-actions-runner: sort inherited lib functions
SigmaSquadron Jan 31, 2026
59721fe
nixos/gitea-actions-runner: use mkMerge for config
SigmaSquadron Jan 31, 2026
3f792e4
nixos/gitea-actions-runner: move helper functions to a scoped let-in …
SigmaSquadron Jan 31, 2026
717e843
nixos/gitea-actions-runner: use lib functions consistently
SigmaSquadron Jan 31, 2026
8d7d648
nixos/gitea-actions-runner: clarify scope of container hypervisor fun…
SigmaSquadron Jan 31, 2026
9bddcc9
nixos/gitea-actions-runner: add sigmasquadron as maintainer
SigmaSquadron Jan 31, 2026
9672c25
nixos/gitea-actions-runner: remove wide with statement from options
SigmaSquadron Jan 31, 2026
07794ea
nixos/gitea-actions-runner: use getExe instead of hardcoding the daem…
SigmaSquadron Jan 31, 2026
0c4559a
nixos/gitea-actions-runner: minor description improvements
SigmaSquadron Jan 31, 2026
fb21b87
nixos/gitea-actions-runner: improve assertion messages
SigmaSquadron Jan 31, 2026
3ddee1d
nixos/gitea-actions-runner: dehardcode gitea branding
SigmaSquadron Jan 31, 2026
5d3e1c8
nixos/act-runner: split off as generic file from gitea-actions-runner
SigmaSquadron Jan 31, 2026
5b59e17
nixos/act-runner: improve option descriptions with links to upstream …
SigmaSquadron Jan 31, 2026
16fd1c0
nixos/forgejo-runner: init
SigmaSquadron Jan 31, 2026
bca1eec
nixos/tests/forgejo: update runner tests
SigmaSquadron Jan 31, 2026
e35bfdc
nixos/doc/release-notes: document forgejo-runner migration path
SigmaSquadron Jan 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2605.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.

- `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.
Expand Down
3 changes: 2 additions & 1 deletion nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Comment on lines +4 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: preserve the same order of arguments as the gitea runner. This order seems more logical so I think the other should be changed.

mainProgram = "forgejo-runner";
})
];
}
315 changes: 315 additions & 0 deletions nixos/modules/services/continuous-integration/act-runner/generic.nix
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Contributor

@gepbird gepbird Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to your changes, but there's a bug here. This only throws, if all the configs are incorrect (in other words it only does NOT throw when there is at least one correct config), it should throw if it finds at least one incorrect config.

Example: this config builds when it should throw instead, because the existing "test" instance is correct, it ignored "test2" being incorrect with both token and tokenFile set:

diff --git a/nixos/tests/forgejo.nix b/nixos/tests/forgejo.nix
index 85221a33c9ee..98d9928bd39d 100644
--- a/nixos/tests/forgejo.nix
+++ b/nixos/tests/forgejo.nix
@@ -72,6 +72,17 @@ let
                 ];
                 tokenFile = "/var/lib/forgejo/runner_token";
               };
+              configuration.services.forgejo-runner.instances."test2" = {
+                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";
+                token = "fake-token";
+              };
             };
             specialisation.dump = {
               inheritParentConfig = true;

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";
Copy link
Contributor

@gepbird gepbird Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When existing forgejo-runner users switch to the new module, the state directory will be changed. I'm not sure if the data loss is significant, I assume it can be recreated without issues and with automatic runner re-registrations.

This is my current data:

[root@raspi5-doboz:/var/lib/gitea-runner/raspi5-doboz]# l
total 20K
drwxr-xr-x 3 nobody nogroup 4.0K Dec 31 02:06 .
drwxr-xr-x 5 nobody nogroup 4.0K Dec 30 18:20 ..
drwxr-xr-x 4 nobody nogroup 4.0K Dec 31 01:28 .cache
-rw-r--r-- 1 nobody nogroup   64 Dec 31 02:06 .labels
-rw-r--r-- 1 nobody nogroup  478 Dec 31 02:06 .runner

[root@raspi5-doboz:/var/lib/gitea-runner/raspi5-doboz]# cat .labels
ubuntu-24.04-arm:docker://ghcr.io/catthehacker/ubuntu:act-24.04

[root@raspi5-doboz:/var/lib/gitea-runner/raspi5-doboz]# cat .runner
{
  "WARNING": "This file is automatically generated by forgejo-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner.",
  "id": 8,
  "uuid": "d2000000-0000-0000-0000-000000000000",
  "name": "raspi5-doboz",
  "token": "b200000000000000000000000000000000000000",
  "address": "https://git.tchfoo.com",
  "labels": [
    "ubuntu-24.04-arm:docker://ghcr.io/catthehacker/ubuntu:act-24.04"
  ]
}

Copy link
Member

@pyrox0 pyrox0 Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the most impactful change will be losing the cache directory, since those can be large and are also used extensively in many folks' CI setups. So it will both duplicate the directory and will make folks' CI pipelines take longer, at least temporarily.

Therefore I would not want this PR backported, along with the concerns that emily mentioned below.

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;
}
))
];
}
Original file line number Diff line number Diff line change
@@ -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";
})
];
}
Loading
Loading