Exit with specified exit code when runner is outdated#4285
Exit with specified exit code when runner is outdated#4285nikola-jokic wants to merge 1 commit intoactions:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for a user-configurable process exit code when the runner is rejected as “outdated”, enabling external orchestrators (e.g., ARC) to distinguish this failure mode from other runner termination cases.
Changes:
- Introduces
ACTIONS_RUNNER_OUTDATED_EXIT_CODEconstant and uses it inRunner.Listenerto return a configurable exit code for “runner outdated” failures. - Updates
run.sh/run.cmdand helper templates to propagate and honor the configured “outdated” exit code instead of normalizing to0.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Runner.Listener/Program.cs | Parses ACTIONS_RUNNER_OUTDATED_EXIT_CODE and uses it when handling the “outdated runner” AccessDenied case. |
| src/Runner.Common/Constants.cs | Adds ACTIONS_RUNNER_OUTDATED_EXIT_CODE to the constants list. |
| src/Misc/layoutroot/run.sh | Exits with the outdated exit code (when configured) so the outer environment can detect it. |
| src/Misc/layoutroot/run.cmd | Adds parsing/validation and propagates the outdated exit code on Windows wrapper. |
| src/Misc/layoutroot/run-helper.sh.template | Propagates the outdated exit code from the listener and exits with it. |
| src/Misc/layoutroot/run-helper.cmd.template | Propagates the outdated exit code from the listener and exits with it. |
| { | ||
| var configuredExitCode = Environment.GetEnvironmentVariable(Constants.Variables.Actions.RunnerOutdatedExitCode); | ||
| if (int.TryParse(configuredExitCode, NumberStyles.Integer, CultureInfo.InvariantCulture, out int exitCode) && | ||
| exitCode > 5) |
There was a problem hiding this comment.
The cutoff exitCode > 5 is meant to avoid clashing with the runner’s well-known return codes, but Constants.Runner.ReturnCode already defines a reserved code RunnerConfigurationRefreshed = 6. To avoid collisions (and future magic-number drift), compare against the highest reserved return code (e.g., Constants.Runner.ReturnCode.RunnerConfigurationRefreshed) rather than hardcoding 5.
| exitCode > 5) | |
| exitCode > (int)Constants.Runner.ReturnCode.RunnerConfigurationRefreshed) |
| var configuredExitCode = Environment.GetEnvironmentVariable(Constants.Variables.Actions.RunnerOutdatedExitCode); | ||
| if (int.TryParse(configuredExitCode, NumberStyles.Integer, CultureInfo.InvariantCulture, out int exitCode) && | ||
| exitCode > 5) | ||
| { | ||
| return exitCode; | ||
| } | ||
|
|
||
| return Constants.Runner.ReturnCode.TerminatedError; |
There was a problem hiding this comment.
On Linux/macOS, process exit codes are limited to 0-255 (values outside that range wrap). If ACTIONS_RUNNER_OUTDATED_EXIT_CODE is set to something >255, the listener will exit with a wrapped value that will not match the configured env var in the wrapper scripts, so the “outdated” handling won’t trigger reliably. Consider validating an upper bound (<=255) when parsing the env var (and document the supported range).
| if [[ $returnCode -eq 2 ]]; then | ||
| echo "Restarting runner..." | ||
| elif [[ -n "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" && "$returnCode" == "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" ]]; then | ||
| echo "Exiting runner..." | ||
| exit "$returnCode" |
There was a problem hiding this comment.
run.sh exits with the configured code whenever returnCode == $ACTIONS_RUNNER_OUTDATED_EXIT_CODE, but it doesn’t validate that the env var is numeric and above the reserved runner exit codes. If a user sets it to 1 (or another well-known code), this changes the wrapper behavior for unrelated failures. Align this with the listener/scripts logic by validating the env var is an integer and greater than the highest reserved return code (currently 6) before using it for comparison.
| exit 0 | ||
| else | ||
| echo "Exiting with unknown error code: ${returnCode}" | ||
| echo "NOO Exiting with unknown error code: ${returnCode}" |
There was a problem hiding this comment.
The log line has an extra "NOO" prefix, which looks accidental and will make diagnostics noisy/confusing. Please remove it.
| echo "NOO Exiting with unknown error code: ${returnCode}" | |
| echo "Exiting with unknown error code: ${returnCode}" |
| ) | ||
|
|
||
| if not "%OUTDATED_EXIT_CODE_NUM%"=="" ( | ||
| if %OUTDATED_EXIT_CODE_NUM% LEQ 5 set OUTDATED_EXIT_CODE_NUM= |
There was a problem hiding this comment.
This script treats values <= 5 as invalid for ACTIONS_RUNNER_OUTDATED_EXIT_CODE, but the runner’s reserved return codes include 6 as well (RunnerConfigurationRefreshed). To avoid collisions, consider treating <= 6 as invalid here (and keep the same rule across all wrappers).
| if %OUTDATED_EXIT_CODE_NUM% LEQ 5 set OUTDATED_EXIT_CODE_NUM= | |
| if %OUTDATED_EXIT_CODE_NUM% LEQ 6 set OUTDATED_EXIT_CODE_NUM= |
| if [[ $returnCode == 0 ]]; then | ||
| echo "Runner listener exit with 0 return code, stop the service, no retry needed." | ||
| exit 0 | ||
| elif [[ "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" =~ ^[0-9]+$ && "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" -gt 5 && "$returnCode" -eq "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" ]]; then |
There was a problem hiding this comment.
The validation here allows ACTIONS_RUNNER_OUTDATED_EXIT_CODE values > 5, but the runner already has a reserved return code 6 (RunnerConfigurationRefreshed). To avoid collisions with reserved codes, update the check to require a value greater than the highest reserved code (currently 6).
| elif [[ "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" =~ ^[0-9]+$ && "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" -gt 5 && "$returnCode" -eq "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" ]]; then | |
| elif [[ "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" =~ ^[0-9]+$ && "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" -gt 6 && "$returnCode" -eq "$ACTIONS_RUNNER_OUTDATED_EXIT_CODE" ]]; then |
| ) | ||
|
|
||
| if not "%OUTDATED_EXIT_CODE_NUM%"=="" ( | ||
| if %OUTDATED_EXIT_CODE_NUM% LEQ 5 SET OUTDATED_EXIT_CODE_NUM= |
There was a problem hiding this comment.
The outdated exit code validation only rejects values <= 5, but the runner’s reserved return codes include 6 as well (RunnerConfigurationRefreshed). To avoid collisions with reserved codes, consider rejecting <= 6 here (and keep the rule consistent with Program.cs and the other wrapper scripts).
| if %OUTDATED_EXIT_CODE_NUM% LEQ 5 SET OUTDATED_EXIT_CODE_NUM= | |
| if %OUTDATED_EXIT_CODE_NUM% LEQ 6 SET OUTDATED_EXIT_CODE_NUM= |
| @@ -143,7 +143,7 @@ private async static Task<int> MainAsync(IHostContext context, string[] args) | |||
| { | |||
| terminal.WriteError($"An error occured: {e.Message}"); | |||
There was a problem hiding this comment.
The error message uses the misspelling "occured"; elsewhere in this file the wording is "occurred". Please fix to "An error occurred" for consistency/professional output.
| terminal.WriteError($"An error occured: {e.Message}"); | |
| terminal.WriteError($"An error occurred: {e.Message}"); |
| if not "%ACTIONS_RUNNER_OUTDATED_EXIT_CODE%"=="" ( | ||
| set /a OUTDATED_EXIT_CODE_NUM=%ACTIONS_RUNNER_OUTDATED_EXIT_CODE% >NUL 2>NUL | ||
| if errorlevel 1 set OUTDATED_EXIT_CODE_NUM= |
There was a problem hiding this comment.
ACTIONS_RUNNER_OUTDATED_EXIT_CODE is expanded directly into the set /a OUTDATED_EXIT_CODE_NUM=%ACTIONS_RUNNER_OUTDATED_EXIT_CODE% expression, which means a maliciously crafted environment variable (e.g., containing & or other command separators) can break out of the arithmetic expression and inject arbitrary additional commands into the batch script. An attacker who can control this environment variable (for example via workflow configuration on a self-hosted runner) could execute commands with the runner's privileges when this script runs. To fix this, ensure the value is strictly validated/sanitized as a numeric exit code before expansion in a set /a context, or use a parsing approach that does not allow command separators to be interpreted by cmd.exe.
| if not "%ACTIONS_RUNNER_OUTDATED_EXIT_CODE%"=="" ( | |
| set /a OUTDATED_EXIT_CODE_NUM=%ACTIONS_RUNNER_OUTDATED_EXIT_CODE% >NUL 2>NUL | |
| if errorlevel 1 set OUTDATED_EXIT_CODE_NUM= | |
| if defined ACTIONS_RUNNER_OUTDATED_EXIT_CODE ( | |
| for /f "tokens=2 delims==" %%A in (' | |
| set ACTIONS_RUNNER_OUTDATED_EXIT_CODE 2^>NUL ^| findstr /R /C:"^ACTIONS_RUNNER_OUTDATED_EXIT_CODE=[0-9][0-9]*$" | |
| ') do set OUTDATED_EXIT_CODE_NUM=%%A | |
| if defined OUTDATED_EXIT_CODE_NUM ( | |
| set /a OUTDATED_EXIT_CODE_NUM=%OUTDATED_EXIT_CODE_NUM% >NUL 2>NUL | |
| if errorlevel 1 set OUTDATED_EXIT_CODE_NUM= | |
| ) |
| SET OUTDATED_EXIT_CODE_NUM= | ||
|
|
||
| if not "%ACTIONS_RUNNER_OUTDATED_EXIT_CODE%"=="" ( | ||
| set /a OUTDATED_EXIT_CODE_NUM=%ACTIONS_RUNNER_OUTDATED_EXIT_CODE% >NUL 2>NUL |
There was a problem hiding this comment.
In this batch helper script, ACTIONS_RUNNER_OUTDATED_EXIT_CODE is interpolated directly into set /a OUTDATED_EXIT_CODE_NUM=%ACTIONS_RUNNER_OUTDATED_EXIT_CODE%, allowing any command separators or metacharacters in the environment variable to be interpreted by cmd.exe rather than treated as data. If an attacker can influence this environment variable (e.g., through workflow or service configuration on a self-hosted runner), they can inject and run arbitrary commands with the runner's privileges when this helper executes. This should be mitigated by strictly constraining the variable to a safe numeric form before it is expanded, or by using a safer pattern that prevents injected &, |, or similar characters from being parsed as additional commands.
| public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS"; | ||
| public static readonly string RequireJobContainer = "ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER"; | ||
| public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG"; | ||
| public static readonly string RunnerOutdatedExitCode = "ACTIONS_RUNNER_OUTDATED_EXIT_CODE"; |
There was a problem hiding this comment.
should we just define an exitcode ourselves and allow customers to get it when they set some enablement ENV?
ex:
export ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE=1
run.sh will return 6 (or whatever code we are free to use next)
export ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE=0
run.sh will return 1 (or whatever code we return today)
There was a problem hiding this comment.
That sounds good to me!
Allow user-defined exit code when the runner is outdated. This change helps ARC determine how to handle runner deprecated failures, as well as allow users to inspect when the runners are exiting with outdated errors.
Outdated exit code must be > 5 in order to not override runner's well known exit codes.