Skip to content

Exit with specified exit code when runner is outdated#4285

Open
nikola-jokic wants to merge 1 commit intoactions:mainfrom
nikola-jokic:nikola-jokic/exit-outdated-exit-code
Open

Exit with specified exit code when runner is outdated#4285
nikola-jokic wants to merge 1 commit intoactions:mainfrom
nikola-jokic:nikola-jokic/exit-outdated-exit-code

Conversation

@nikola-jokic
Copy link
Contributor

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.

@nikola-jokic nikola-jokic requested a review from a team as a code owner March 6, 2026 14:21
Copilot AI review requested due to automatic review settings March 6, 2026 14:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_CODE constant and uses it in Runner.Listener to return a configurable exit code for “runner outdated” failures.
  • Updates run.sh / run.cmd and helper templates to propagate and honor the configured “outdated” exit code instead of normalizing to 0.

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)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
exitCode > 5)
exitCode > (int)Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +171
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;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines 20 to +24
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"
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
exit 0
else
echo "Exiting with unknown error code: ${returnCode}"
echo "NOO Exiting with unknown error code: ${returnCode}"
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The log line has an extra "NOO" prefix, which looks accidental and will make diagnostics noisy/confusing. Please remove it.

Suggested change
echo "NOO Exiting with unknown error code: ${returnCode}"
echo "Exiting with unknown error code: ${returnCode}"

Copilot uses AI. Check for mistakes.
)

if not "%OUTDATED_EXIT_CODE_NUM%"=="" (
if %OUTDATED_EXIT_CODE_NUM% LEQ 5 set OUTDATED_EXIT_CODE_NUM=
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
if %OUTDATED_EXIT_CODE_NUM% LEQ 5 set OUTDATED_EXIT_CODE_NUM=
if %OUTDATED_EXIT_CODE_NUM% LEQ 6 set OUTDATED_EXIT_CODE_NUM=

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
)

if not "%OUTDATED_EXIT_CODE_NUM%"=="" (
if %OUTDATED_EXIT_CODE_NUM% LEQ 5 SET OUTDATED_EXIT_CODE_NUM=
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
if %OUTDATED_EXIT_CODE_NUM% LEQ 5 SET OUTDATED_EXIT_CODE_NUM=
if %OUTDATED_EXIT_CODE_NUM% LEQ 6 SET OUTDATED_EXIT_CODE_NUM=

Copilot uses AI. Check for mistakes.
@@ -143,7 +143,7 @@ private async static Task<int> MainAsync(IHostContext context, string[] args)
{
terminal.WriteError($"An error occured: {e.Message}");
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
terminal.WriteError($"An error occured: {e.Message}");
terminal.WriteError($"An error occurred: {e.Message}");

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
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=
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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=
)

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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";
Copy link
Member

Choose a reason for hiding this comment

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

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants