From 8bc84859a45265feaccf4624d38915ec6df3516e Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 14 Jul 2026 11:00:01 -0600 Subject: [PATCH] Report exit code when a node action fails without reporting an error Report the exit code when a node or docker action exits non-zero but recorded no error of its own, instead of failing the step with an empty log. A crash, an OOM kill, or an external termination can end the process before it reports anything. Actions that already report their failure are left alone. ScriptHandler already does this for `run:` steps. This is the symptom in actions/cache#1754, where Windows runners die inside actions/cache with no output at all. The likely cause is the V8 Maglev crash worked around in #4447 (nodejs/node#62260), which kills node before any JavaScript runs. Reporting the exit code surfaces it as 0xC0000409. GetErrorCount() is compared before and after the run because embedded steps write to a collector shared with the enclosing context. --- src/Runner.Worker/ExecutionContext.cs | 8 ++++++++ src/Runner.Worker/Handlers/ContainerActionHandler.cs | 6 ++++++ src/Runner.Worker/Handlers/NodeScriptActionHandler.cs | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 0f9410821c6..4a96103fb43 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -116,6 +116,7 @@ public interface IExecutionContext : IRunnerService void FlushDeferredOutcomeConclusion(); void AddIssue(Issue issue, ExecutionContextLogOptions logOptions); + int GetErrorCount(); void Progress(int percentage, string currentOperation = null); void UpdateDetailTimelineRecord(TimelineRecord record); @@ -238,6 +239,13 @@ private ExecutionContext(ExecutionContext parent, bool embedded) // as its enclosing execution context. public bool IsEmbedded { get; private init; } + public int GetErrorCount() + { + return IsEmbedded + ? (_embeddedIssueCollector?.Count(x => x.Type == IssueType.Error) ?? 0) + : _record.ErrorCount; + } + public TaskResult? Result { get diff --git a/src/Runner.Worker/Handlers/ContainerActionHandler.cs b/src/Runner.Worker/Handlers/ContainerActionHandler.cs index 099495cb464..4a74634f68c 100644 --- a/src/Runner.Worker/Handlers/ContainerActionHandler.cs +++ b/src/Runner.Worker/Handlers/ContainerActionHandler.cs @@ -266,10 +266,16 @@ public async Task RunAsync(ActionRunStage stage) using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager, container)) using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager, container)) { + var errorCountBeforeRun = ExecutionContext.GetErrorCount(); var runExitCode = await dockerManager.DockerRun(ExecutionContext, container, stdoutManager.OnDataReceived, stderrManager.OnDataReceived); ExecutionContext.Debug($"Docker Action run completed with exit code {runExitCode}"); if (runExitCode != 0) { + if (ExecutionContext.GetErrorCount() == errorCountBeforeRun) + { + ExecutionContext.Error($"Docker Action run completed with exit code {runExitCode}."); + } + ExecutionContext.Result = TaskResult.Failed; } } diff --git a/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs b/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs index 85ff32777f7..5cdacbba45e 100644 --- a/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs +++ b/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs @@ -160,6 +160,8 @@ public async Task RunAsync(ActionRunStage stage) StepHost.OutputDataReceived += stdoutManager.OnDataReceived; StepHost.ErrorDataReceived += stderrManager.OnDataReceived; + var errorCountBeforeRun = ExecutionContext.GetErrorCount(); + // Execute the process. Exit code 0 should always be returned. // A non-zero exit code indicates infrastructural failure. // Task failure should be communicated over STDOUT using ## commands. @@ -188,6 +190,11 @@ public async Task RunAsync(ActionRunStage stage) ExecutionContext.Debug($"Node Action run completed with exit code {exitCode}"); if (exitCode != 0) { + if (ExecutionContext.GetErrorCount() == errorCountBeforeRun) + { + ExecutionContext.Error($"Node Action run completed with exit code {exitCode}."); + } + ExecutionContext.Result = TaskResult.Failed; } }