push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
{
lib,
pkgs,
...
}:
{
options.services.github-runners = lib.mkOption {
description = ''
Multiple GitHub Runners.
'';
example = {
runner1 = {
enable = true;
url = "https://github.com/owner/repo";
name = "runner1";
tokenFile = "/secrets/token1";
};
runner2 = {
enable = true;
url = "https://github.com/owner/repo";
name = "runner2";
tokenFile = "/secrets/token2";
};
};
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ name, config, ... }:
{
options = {
enable = lib.mkOption {
default = false;
example = true;
description = ''
Whether to enable GitHub Actions runner.
Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
[About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).
'';
type = lib.types.bool;
};
url = lib.mkOption {
type = lib.types.str;
description = ''
Repository to add the runner to.
Changing this option triggers a new runner registration.
IMPORTANT: If your token is org-wide (not per repository), you need to
provide a github org link, not a single repository, so do it like this
`https://github.com/nixos`, not like this
`https://github.com/nixos/nixpkgs`.
Otherwise, you are going to get a `404 NotFound`
from `POST https://api.github.com/actions/runner-registration`
in the configure script.
'';
example = "https://github.com/nixos/nixpkgs";
};
tokenFile = lib.mkOption {
type = lib.types.path;
description = ''
The full path to a file which contains either
* a fine-grained personal access token (PAT),
* a classic PAT
* or a runner registration token
Changing this option or the `tokenFile`s content triggers a new runner registration.
We suggest using the fine-grained PATs. A runner registration token is valid
only for 1 hour after creation, so the next time the runner configuration changes
this will give you hard-to-debug HTTP 404 errors in the configure step.
The file should contain exactly one line with the token without any newline.
(Use `echo -n 'token' > token file` to make sure no newlines sneak in.)
If the file contains a PAT, the service creates a new registration token
on startup as needed.
If a registration token is given, it can be used to re-register a runner of the same
name but is time-limited as noted above.
For fine-grained PATs:
Give it "Read and Write access to organization/repository self hosted runners",
depending on whether it is organization wide or per-repository. You might have to
experiment a little, fine-grained PATs are a `beta` Github feature and still subject
to change; nonetheless they are the best option at the moment.
For classic PATs:
Make sure the PAT has a scope of `admin:org` for organization-wide registrations
or a scope of `repo` for a single repository.
For runner registration tokens:
Nothing special needs to be done, but updating will break after one hour,
so these are not recommended.
'';
example = "/run/secrets/github-runner/nixos.token";
};
name = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Name of the runner to configure. If null, defaults to the hostname.
Changing this option triggers a new runner registration.
'';
example = "nixos";
default = name;
};
runnerGroup = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Name of the runner group to add this runner to (defaults to the default runner group).
Changing this option triggers a new runner registration.
'';
default = null;
};
extraLabels = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
Extra labels in addition to the default (unless disabled through the `noDefaultLabels` option).
Changing this option triggers a new runner registration.
'';
example = lib.literalExpression ''[ "nixos" ]'';
default = [ ];
};
noDefaultLabels = lib.mkOption {
type = lib.types.bool;
description = ''
Disables adding the default labels. Also see the `extraLabels` option.
Changing this option triggers a new runner registration.
'';
default = false;
};
replace = lib.mkOption {
type = lib.types.bool;
description = ''
Replace any existing runner with the same name.
Without this flag, registering a new runner with the same name fails.
'';
default = false;
};
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
description = ''
Extra packages to add to `PATH` of the service to make them available to workflows.
'';
default = [ ];
};
extraEnvironment = lib.mkOption {
type = lib.types.attrs;
description = ''
Extra environment variables to set for the runner, as an attrset.
'';
example = {
GIT_CONFIG = "/path/to/git/config";
};
default = { };
};
serviceOverrides = lib.mkOption {
type = lib.types.attrs;
description = ''
Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
See {manpage}`systemd.exec(5)` for more options.
'';
example = {
ProtectHome = false;
RestrictAddressFamilies = [ "AF_PACKET" ];
};
default = { };
};
package = lib.mkPackageOption pkgs "github-runner" { } // {
apply =
# Support old github-runner versions which don't have the `nodeRuntimes` arg yet.
pkg: pkg.override (old: lib.optionalAttrs (old ? nodeRuntimes) { inherit (config) nodeRuntimes; });
};
ephemeral = lib.mkOption {
type = lib.types.bool;
description = ''
If enabled, causes the following behavior:
- Passes the `--ephemeral` flag to the runner configuration script
- De-registers and stops the runner with GitHub after it has processed one job
- On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option)
- Restarts the service after its successful exit
- On start, wipes the state directory and configures a new runner
You should only enable this option if `tokenFile` points to a file which contains a
personal access token (PAT). If you're using the option with a registration token, restarting the
service will fail as soon as the registration token expired.
Changing this option triggers a new runner registration.
'';
default = false;
};
user = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
User under which to run the service.
If this option and the `group` option is set to `null`,
the service runs as a dynamically allocated user.
Also see the `group` option for an overview on the effects of the `user` and `group` settings.
'';
default = null;
defaultText = lib.literalExpression "username";
};
group = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Group under which to run the service.
The effect of this option depends on the value of the `user` option:
- `group == null` and `user == null`:
The service runs with a dynamically allocated user and group.
- `group == null` and `user != null`:
The service runs as the given user and its default group.
- `group != null` and `user == null`:
This configuration is invalid. In this case, the service would use the given group
but run as root implicitly. If this is really what you want, set `user = "root"` explicitly.
'';
default = null;
defaultText = lib.literalExpression "groupname";
};
workDir = lib.mkOption {
type = with lib.types; nullOr str;
description = ''
Working directory, available as `$GITHUB_WORKSPACE` during workflow runs
and used as a default for [repository checkouts](https://github.com/actions/checkout).
The service cleans this directory on every service start.
A value of `null` will default to the systemd `RuntimeDirectory`.
Changing this option triggers a new runner registration.
'';
default = null;
};
nodeRuntimes = lib.mkOption {
type =
with lib.types;
nonEmptyListOf (enum [
"node20"
"node24"
]);
default = [
"node20"
"node24"
];
description = ''
List of Node.js runtimes the runner should support.
'';
};
};
}
)
);
};
}

View File

@@ -0,0 +1,333 @@
{
config,
lib,
pkgs,
...
}:
{
config.assertions = lib.flatten (
lib.flip lib.mapAttrsToList config.services.github-runners (
name: cfg:
map (lib.mkIf cfg.enable) [
{
assertion = !cfg.noDefaultLabels || (cfg.extraLabels != [ ]);
message = "`services.github-runners.${name}`: The `extraLabels` option is mandatory if `noDefaultLabels` is set";
}
{
assertion = cfg.group == null || cfg.user != null;
message = ''`services.github-runners.${name}`: Setting `group` while leaving `user` unset runs the service as `root`. If this is really what you want, set `user = "root"` explicitly'';
}
]
)
);
config.systemd.services =
let
enabledRunners = lib.filterAttrs (_: cfg: cfg.enable) config.services.github-runners;
in
(lib.flip lib.mapAttrs' enabledRunners (
name: cfg:
let
svcName = "github-runner-${name}";
systemdDir = "github-runner/${name}";
# %t: Runtime directory root (usually /run); see systemd.unit(5)
runtimeDir = "%t/${systemdDir}";
# %S: State directory root (usually /var/lib); see systemd.unit(5)
stateDir = "%S/${systemdDir}";
# %L: Log directory root (usually /var/log); see systemd.unit(5)
logsDir = "%L/${systemdDir}";
# Name of file stored in service state directory
currentConfigTokenFilename = ".current-token";
workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
in
lib.nameValuePair svcName {
description = "GitHub Actions runner";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network.target"
"network-online.target"
];
environment = {
HOME = workDir;
RUNNER_ROOT = stateDir;
}
// cfg.extraEnvironment;
path =
(with pkgs; [
bashInteractive
coreutils
git
gnutar
gzip
])
++ [
config.nix.package
]
++ cfg.extraPackages;
serviceConfig = lib.mkMerge [
{
ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
# Does the following, sequentially:
# - If the module configuration or the token has changed, purge the state directory,
# and create the current and the new token file with the contents of the configured
# token. While both files have the same content, only the later is accessible by
# the service user.
# - Configure the runner using the new token file. When finished, delete it.
# - Set up the directory structure by creating the necessary symlinks.
ExecStartPre =
let
# Wrapper script which expects the full path of the state, working and logs
# directory as arguments. Overrides the respective systemd variables to provide
# unambiguous directory names. This becomes relevant, for example, if the
# caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
# to contain more than one directory. This causes systemd to set the respective
# environment variables with the path of all of the given directories, separated
# by a colon.
writeScript =
name: lines:
pkgs.writeShellScript "${svcName}-${name}.sh" ''
set -euo pipefail
STATE_DIRECTORY="$1"
WORK_DIRECTORY="$2"
LOGS_DIRECTORY="$3"
${lines}
'';
runnerRegistrationConfig = lib.getAttrs [
"ephemeral"
"extraLabels"
"name"
"noDefaultLabels"
"runnerGroup"
"tokenFile"
"url"
"workDir"
] cfg;
newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
newConfigTokenPath = "$STATE_DIRECTORY/.new-token";
currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
runnerCredFiles = [
".credentials"
".credentials_rsaparams"
".runner"
];
unconfigureRunner = writeScript "unconfigure" ''
copy_tokens() {
# Copy the configured token file to the state dir and allow the service user to read the file
install --mode=666 ${lib.escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
# Also copy current file to allow for a diff on the next start
install --mode=600 ${lib.escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
}
clean_state() {
find "$STATE_DIRECTORY/" -mindepth 1 -delete
copy_tokens
}
diff_config() {
changed=0
# Check for module config changes
[[ -f "${currentConfigPath}" ]] \
&& ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
|| changed=1
# Also check the content of the token file
[[ -f "${currentConfigTokenPath}" ]] \
&& ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${lib.escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
|| changed=1
# If the config has changed, remove old state and copy tokens
if [[ "$changed" -eq 1 ]]; then
echo "Config has changed, removing old runner state."
echo "The old runner will still appear in the GitHub Actions UI." \
"You have to remove it manually."
clean_state
fi
}
if [[ "${lib.optionalString cfg.ephemeral "1"}" ]]; then
# In ephemeral mode, we always want to start with a clean state
clean_state
elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
# There are state files from a previous run; diff them to decide if we need a new registration
diff_config
else
# The state directory is entirely empty which indicates a first start
copy_tokens
fi
# Always clean workDir
find -H "$WORK_DIRECTORY" -mindepth 1 -delete
'';
configureRunner =
writeScript "configure" # bash
''
if [[ -e "${newConfigTokenPath}" ]]; then
echo "Configuring GitHub Actions Runner"
# shellcheck disable=SC2054 # don't complain about commas in --labels
args=(
--unattended
--disableupdate
--work "$WORK_DIRECTORY"
--url ${lib.escapeShellArg cfg.url}
--labels ${lib.escapeShellArg (lib.concatStringsSep "," cfg.extraLabels)}
${lib.optionalString (cfg.name != null) "--name ${lib.escapeShellArg cfg.name}"}
${lib.optionalString cfg.replace "--replace"}
${lib.optionalString (
cfg.runnerGroup != null
) "--runnergroup ${lib.escapeShellArg cfg.runnerGroup}"}
${lib.optionalString cfg.ephemeral "--ephemeral"}
${lib.optionalString cfg.noDefaultLabels "--no-default-labels"}
)
# If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
# if it is not a PAT, we assume it contains a registration token and use the --token option
token=$(<"${newConfigTokenPath}")
if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
args+=(--pat "$token")
else
args+=(--token "$token")
fi
${cfg.package}/bin/Runner.Listener configure "''${args[@]}"
# Move the automatically created _diag dir to the logs dir
mkdir -p "$STATE_DIRECTORY/_diag"
cp -r "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
rm -rf "$STATE_DIRECTORY/_diag/"
# Cleanup token from config
rm "${newConfigTokenPath}"
# Symlink to new config
ln -s '${newConfigPath}' "${currentConfigPath}"
fi
'';
setupWorkDir = writeScript "setup-work-dirs" ''
# Link _diag dir
ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag"
# Link the runner credentials to the work dir
ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/"
'';
in
map
(
x:
"${x} ${
lib.escapeShellArgs [
stateDir
workDir
logsDir
]
}"
)
[
"+${unconfigureRunner}" # runs as root
configureRunner
setupWorkDir
];
# If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
# to trigger a fresh registration.
Restart = if cfg.ephemeral then "on-success" else "no";
# If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
# https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
RestartForceExitStatus = [ 2 ];
# Contains _diag
LogsDirectory = [ systemdDir ];
# Default RUNNER_ROOT which contains ephemeral Runner data
RuntimeDirectory = [ systemdDir ];
# Home of persistent runner data, e.g., credentials
StateDirectory = [ systemdDir ];
StateDirectoryMode = "0700";
WorkingDirectory = workDir;
InaccessiblePaths = [
# Token file path given in the configuration, if visible to the service
"-${cfg.tokenFile}"
# Token file in the state directory
"${stateDir}/${currentConfigTokenFilename}"
];
KillSignal = "SIGINT";
# Hardening (may overlap with DynamicUser=)
# The following options are only for optimizing:
# systemd-analyze security github-runner
AmbientCapabilities = lib.mkBefore [ "" ];
CapabilityBoundingSet = lib.mkBefore [ "" ];
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = lib.mkBefore [ "" ];
NoNewPrivileges = lib.mkDefault true;
PrivateDevices = lib.mkDefault true;
PrivateMounts = lib.mkDefault true;
PrivateTmp = lib.mkDefault true;
PrivateUsers = lib.mkDefault true;
ProtectClock = lib.mkDefault true;
ProtectControlGroups = lib.mkDefault true;
ProtectHome = lib.mkDefault true;
ProtectHostname = lib.mkDefault true;
ProtectKernelLogs = lib.mkDefault true;
ProtectKernelModules = lib.mkDefault true;
ProtectKernelTunables = lib.mkDefault true;
ProtectSystem = lib.mkDefault "strict";
RemoveIPC = lib.mkDefault true;
RestrictNamespaces = lib.mkDefault true;
RestrictRealtime = lib.mkDefault true;
RestrictSUIDSGID = lib.mkDefault true;
UMask = lib.mkDefault "0066";
ProtectProc = lib.mkDefault "invisible";
SystemCallFilter = lib.mkBefore [
"~@clock"
"~@cpu-emulation"
"~@module"
"~@mount"
"~@obsolete"
"~@raw-io"
"~@reboot"
"~capset"
"~setdomainname"
"~sethostname"
];
RestrictAddressFamilies = lib.mkBefore [
"AF_INET"
"AF_INET6"
"AF_UNIX"
"AF_NETLINK"
];
BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ];
# Needs network access
PrivateNetwork = lib.mkDefault false;
# Cannot be true due to Node
MemoryDenyWriteExecute = lib.mkDefault false;
# The more restrictive "pid" option makes `nix` commands in CI emit
# "GC Warning: Couldn't read /proc/stat"
# You may want to set this to "pid" if not using `nix` commands
ProcSubset = lib.mkDefault "all";
# Coverage programs for compiled code such as `cargo-tarpaulin` disable
# ASLR (address space layout randomization) which requires the
# `personality` syscall
# You may want to set this to `true` if not using coverage tooling on
# compiled code
LockPersonality = lib.mkDefault false;
DynamicUser = lib.mkDefault true;
}
(lib.mkIf (cfg.user != null) {
DynamicUser = false;
User = cfg.user;
})
(lib.mkIf (cfg.group != null) {
DynamicUser = false;
Group = cfg.group;
})
cfg.serviceOverrides
];
}
));
}