Change:
After rebooting the machine, the systemd service fails to start due to an "invalid user: ‘github-runner’" Steps to reproduce the behavior: The systemd service enters the state : It does not happen after every reboot. I have not found the circumstances that make the systemd service fail. One workaround to bring the service back to the running state is to manually restart . Please run and paste the result. Maintainer information:
Thanks for raising this. I guess I ran into this as well already but didn't give it too much thought. Now, after reading systemd.service(5) again, I believe we should prefix with instead of : I'm not sure that's really the issue but it seems like the user/group () is not always set up when systemd runs . It appears would make sure they exist. I'm going to open a PR. Regardless of fixing this issue (which I hope it does), it makes sense to use instead of .
Wow, thanks for the quick response! I will check out your PR soon.

Discussion:
stateDir = "%S/${systemdDir}"; # %L: Log directory root (usually /var/log); see systemd.unit(5) logsDir = "%L/${systemdDir}"; <ins> # Name of file stored in service state directory currentConfigTokenFilename = ".current-token"; </ins> in { options.services.github-runner = {

ExecStart = "${cfg.package}/bin/runsvc.sh"; # Does the following, sequentially: <del> # - Copy the current and the previous `tokenFile` to the $RUNTIME_DIRECTORY #   and make it accessible to the service user to allow for a content #   comparison. # - If the module configuration or the token has changed, clear the state directory. # - Configure the runner. # - Copy the configured `tokenFile` to the $STATE_DIRECTORY and make it #   inaccessible to the service user. </del> <ins> # - 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. </ins> # - Set up the directory structure by creating the necessary symlinks. ExecStartPre = let

currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json"; runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" ] cfg; newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig); <del> currentConfigTokenFilename = ".current-token"; </del> newConfigTokenFilename = ".new-token"; runnerCredFiles = [ ".credentials" ".credentials_rsaparams" ".runner" ]; <del> ownConfigTokens = writeScript "own-config-tokens" '' # Copy current and new token file to runtime dir and make it accessible to the service user cp ${escapeShellArg cfg.tokenFile} "$RUNTIME_DIRECTORY/${newConfigTokenFilename}" chmod 600 "$RUNTIME_DIRECTORY/${newConfigTokenFilename}" chown "$USER" "$RUNTIME_DIRECTORY/${newConfigTokenFilename}" if [[ -e "$STATE_DIRECTORY/${currentConfigTokenFilename}" ]]; then cp "$STATE_DIRECTORY/${currentConfigTokenFilename}" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}" chmod 600 "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}" chown "$USER" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}" fi ''; disownConfigTokens = writeScript "disown-config-tokens" '' # Make the token inaccessible to the runner service user chmod 600 "$STATE_DIRECTORY/${currentConfigTokenFilename}" chown root:root "$STATE_DIRECTORY/${currentConfigTokenFilename}" ''; </del> unconfigureRunner = writeScript "unconfigure" '' differs= # Set `differs = 1` if current and new runner config differ or if `currentConfigPath` does not exist ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 || differs=1 # Also trigger a registration if the token content changed ${pkgs.diffutils}/bin/diff -q  <del> "$RUNTIME_DIRECTORY"/{${currentConfigTokenFilename},${newConfigTokenFilename}}  </del> <ins> "$STATE_DIRECTORY"/${currentConfigTokenFilename}  ${escapeShellArg cfg.tokenFile}  </ins> >/dev/null 2>&1 || differs=1 if [[ -n "$differs" ]]; then

echo "The old runner will still appear in the GitHub Actions UI."  "You have to remove it manually." find "$STATE_DIRECTORY/" -mindepth 1 -delete <ins> # Copy the configured token file to the state dir and allow the service user to read the file install --mode=666 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${newConfigTokenFilename}" # Also copy current file to allow for a diff on the next start install --mode=600 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${currentConfigTokenFilename}" </ins> fi ''; configureRunner = writeScript "configure" '' <del> empty=$(ls -A "$STATE_DIRECTORY") if [[ -z "$empty" ]]; then </del> <ins> if [[ -e "$STATE_DIRECTORY/${newConfigTokenFilename}" ]]; then </ins> echo "Configuring GitHub Actions Runner" <del> token=$(< "$RUNTIME_DIRECTORY"/${newConfigTokenFilename}) </del> <ins> token=$(< "$STATE_DIRECTORY"/${newConfigTokenFilename}) </ins> RUNNER_ROOT="$STATE_DIRECTORY" ${cfg.package}/bin/config.sh  --unattended  --work "$RUNTIME_DIRECTORY" 

rm    -rf "$STATE_DIRECTORY/_diag/" # Cleanup token from config <del> rm -f "$RUNTIME_DIRECTORY"/${currentConfigTokenFilename} mv    "$RUNTIME_DIRECTORY"/${newConfigTokenFilename} "$STATE_DIRECTORY/${currentConfigTokenFilename}" </del> <ins> rm "$STATE_DIRECTORY/${newConfigTokenFilename}" </ins> # Symlink to new config ln -s '${newConfigPath}' "${currentConfigPath}"

''; in map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [ <del> "+${ownConfigTokens}" # runs as root unconfigureRunner </del> <ins> "+${unconfigureRunner}" # runs as root </ins> configureRunner <del> "+${disownConfigTokens}" # runs as root </del> setupRuntimeDir ];

StateDirectoryMode = "0700"; WorkingDirectory = runtimeDir; <ins> InaccessiblePaths = [ # Token file path given in the configuration cfg.tokenFile # Token file in the state directory "${stateDir}/${currentConfigTokenFilename}" ]; </ins> # By default, use a dynamically allocated user DynamicUser = true;