Support return job result as exitcode in hosted runner. (#4233)

This commit is contained in:
Tingluo Huang
2026-02-10 09:31:10 -05:00
committed by GitHub
parent a4a19b152e
commit cdb77c6804
4 changed files with 47 additions and 25 deletions
+13 -10
View File
@@ -24,7 +24,7 @@ namespace GitHub.Runner.Listener
public interface IJobDispatcher : IRunnerService public interface IJobDispatcher : IRunnerService
{ {
bool Busy { get; } bool Busy { get; }
TaskCompletionSource<bool> RunOnceJobCompleted { get; } TaskCompletionSource<TaskResult> RunOnceJobCompleted { get; }
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false); void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
bool Cancel(JobCancelMessage message); bool Cancel(JobCancelMessage message);
Task WaitAsync(CancellationToken token); Task WaitAsync(CancellationToken token);
@@ -56,7 +56,7 @@ namespace GitHub.Runner.Listener
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT // timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
private TimeSpan _channelTimeout; private TimeSpan _channelTimeout;
private TaskCompletionSource<bool> _runOnceJobCompleted = new(); private TaskCompletionSource<TaskResult> _runOnceJobCompleted = new();
public event EventHandler<JobStatusEventArgs> JobStatus; public event EventHandler<JobStatusEventArgs> JobStatus;
@@ -82,7 +82,7 @@ namespace GitHub.Runner.Listener
Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds."); Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds.");
} }
public TaskCompletionSource<bool> RunOnceJobCompleted => _runOnceJobCompleted; public TaskCompletionSource<TaskResult> RunOnceJobCompleted => _runOnceJobCompleted;
public bool Busy { get; private set; } public bool Busy { get; private set; }
@@ -340,18 +340,19 @@ namespace GitHub.Runner.Listener
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken) private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
{ {
var jobResult = TaskResult.Succeeded;
try try
{ {
await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken); jobResult = await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
} }
finally finally
{ {
Trace.Info("Fire signal for one time used runner."); Trace.Info("Fire signal for one time used runner.");
_runOnceJobCompleted.TrySetResult(true); _runOnceJobCompleted.TrySetResult(jobResult);
} }
} }
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken) private async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
{ {
Busy = true; Busy = true;
try try
@@ -399,7 +400,7 @@ namespace GitHub.Runner.Listener
{ {
// renew job request task complete means we run out of retry for the first job request renew. // renew job request task complete means we run out of retry for the first job request renew.
Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker."); Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker.");
return; return TaskResult.Abandoned;
} }
if (jobRequestCancellationToken.IsCancellationRequested) if (jobRequestCancellationToken.IsCancellationRequested)
@@ -412,7 +413,7 @@ namespace GitHub.Runner.Listener
// complete job request with result Cancelled // complete job request with result Cancelled
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled); await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
return; return TaskResult.Canceled;
} }
HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}"); HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}");
@@ -523,7 +524,7 @@ namespace GitHub.Runner.Listener
await renewJobRequest; await renewJobRequest;
// not finish the job request since the job haven't run on worker at all, we will not going to set a result to server. // not finish the job request since the job haven't run on worker at all, we will not going to set a result to server.
return; return TaskResult.Failed;
} }
// we get first jobrequest renew succeed and start the worker process with the job message. // we get first jobrequest renew succeed and start the worker process with the job message.
@@ -604,7 +605,7 @@ namespace GitHub.Runner.Listener
Trace.Error(detailInfo); Trace.Error(detailInfo);
} }
return; return TaskResultUtil.TranslateFromReturnCode(returnCode);
} }
else if (completedTask == renewJobRequest) else if (completedTask == renewJobRequest)
{ {
@@ -706,6 +707,8 @@ namespace GitHub.Runner.Listener
// complete job request // complete job request
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel); await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
return resultOnAbandonOrCancel;
} }
finally finally
{ {
+22 -4
View File
@@ -324,8 +324,11 @@ namespace GitHub.Runner.Listener
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault"); HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
} }
// hosted runner only run one job and would like to know the result of the job for telemetry and alerting on failure spike.
var returnJobResultForHosted = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED"));
// Run the runner interactively or as service // Run the runner interactively or as service
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral); return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral || returnJobResultForHosted, returnJobResultForHosted);
} }
else else
{ {
@@ -401,7 +404,7 @@ namespace GitHub.Runner.Listener
} }
//create worker manager, create message listener and start listening to the queue //create worker manager, create message listener and start listening to the queue
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false) private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false, bool returnRunOnceJobResult = false)
{ {
try try
{ {
@@ -580,6 +583,21 @@ namespace GitHub.Runner.Listener
Trace.Info($"Ignore any exception after cancel message loop. {ex}"); Trace.Info($"Ignore any exception after cancel message loop. {ex}");
} }
if (returnRunOnceJobResult)
{
try
{
var jobResult = await jobDispatcher.RunOnceJobCompleted.Task;
return TaskResultUtil.TranslateToReturnCode(jobResult);
}
catch (Exception ex)
{
Trace.Error("run once job finished with error.");
Trace.Error(ex);
return Constants.Runner.ReturnCode.TerminatedError;
}
}
return Constants.Runner.ReturnCode.Success; return Constants.Runner.ReturnCode.Success;
} }
} }
@@ -866,14 +884,14 @@ namespace GitHub.Runner.Listener
return Constants.Runner.ReturnCode.Success; return Constants.Runner.ReturnCode.Success;
} }
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce) private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce, bool returnRunOnceJobResult)
{ {
int returnCode = Constants.Runner.ReturnCode.Success; int returnCode = Constants.Runner.ReturnCode.Success;
bool restart = false; bool restart = false;
do do
{ {
restart = false; restart = false;
returnCode = await RunAsync(settings, runOnce); returnCode = await RunAsync(settings, runOnce, returnRunOnceJobResult);
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed) if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
{ {
+2 -1
View File
@@ -739,7 +739,8 @@ namespace GitHub.Runner.Common.Tests.Listener
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent."); Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent.");
if (jobDispatcher.RunOnceJobCompleted.Task.IsCompleted) if (jobDispatcher.RunOnceJobCompleted.Task.IsCompleted)
{ {
Assert.True(await jobDispatcher.RunOnceJobCompleted.Task, "JobDispatcher should set task complete token to 'TRUE' for one time agent."); var result = await jobDispatcher.RunOnceJobCompleted.Task;
Assert.Equal(TaskResult.Succeeded, result);
} }
} }
} }
+10 -10
View File
@@ -295,13 +295,13 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>())) _messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var runOnceJobCompleted = new TaskCompletionSource<bool>(); var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
_jobDispatcher.Setup(x => x.RunOnceJobCompleted) _jobDispatcher.Setup(x => x.RunOnceJobCompleted)
.Returns(runOnceJobCompleted); .Returns(runOnceJobCompleted);
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>())) _jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
.Callback(() => .Callback(() =>
{ {
runOnceJobCompleted.TrySetResult(true); runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
}); });
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>())) _jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() => .Callback(() =>
@@ -399,13 +399,13 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>())) _messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var runOnceJobCompleted = new TaskCompletionSource<bool>(); var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
_jobDispatcher.Setup(x => x.RunOnceJobCompleted) _jobDispatcher.Setup(x => x.RunOnceJobCompleted)
.Returns(runOnceJobCompleted); .Returns(runOnceJobCompleted);
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>())) _jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
.Callback(() => .Callback(() =>
{ {
runOnceJobCompleted.TrySetResult(true); runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
}); });
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>())) _jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() => .Callback(() =>
@@ -733,8 +733,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false); _configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>(); var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(true); completedTask.SetResult(TaskResult.Succeeded);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask); _jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act //Act
@@ -834,8 +834,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false); _configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>(); var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(true); completedTask.SetResult(TaskResult.Succeeded);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask); _jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act //Act
@@ -954,8 +954,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false); _configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>(); var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(true); completedTask.SetResult(TaskResult.Succeeded);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask); _jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act //Act