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

View File

@@ -24,7 +24,7 @@ namespace GitHub.Runner.Listener
public interface IJobDispatcher : IRunnerService
{
bool Busy { get; }
TaskCompletionSource<bool> RunOnceJobCompleted { get; }
TaskCompletionSource<TaskResult> RunOnceJobCompleted { get; }
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
bool Cancel(JobCancelMessage message);
Task WaitAsync(CancellationToken token);
@@ -56,7 +56,7 @@ namespace GitHub.Runner.Listener
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
private TimeSpan _channelTimeout;
private TaskCompletionSource<bool> _runOnceJobCompleted = new();
private TaskCompletionSource<TaskResult> _runOnceJobCompleted = new();
public event EventHandler<JobStatusEventArgs> JobStatus;
@@ -82,7 +82,7 @@ namespace GitHub.Runner.Listener
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; }
@@ -340,18 +340,19 @@ namespace GitHub.Runner.Listener
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
{
var jobResult = TaskResult.Succeeded;
try
{
await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
jobResult = await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
}
finally
{
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;
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.
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)
@@ -412,7 +413,7 @@ namespace GitHub.Runner.Listener
// complete job request with result Cancelled
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
return;
return TaskResult.Canceled;
}
HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}");
@@ -523,7 +524,7 @@ namespace GitHub.Runner.Listener
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.
return;
return TaskResult.Failed;
}
// 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);
}
return;
return TaskResultUtil.TranslateFromReturnCode(returnCode);
}
else if (completedTask == renewJobRequest)
{
@@ -706,6 +707,8 @@ namespace GitHub.Runner.Listener
// complete job request
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
return resultOnAbandonOrCancel;
}
finally
{

View File

@@ -324,8 +324,11 @@ namespace GitHub.Runner.Listener
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
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral || returnJobResultForHosted, returnJobResultForHosted);
}
else
{
@@ -401,7 +404,7 @@ namespace GitHub.Runner.Listener
}
//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
{
@@ -580,6 +583,21 @@ namespace GitHub.Runner.Listener
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;
}
}
@@ -866,14 +884,14 @@ namespace GitHub.Runner.Listener
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;
bool restart = false;
do
{
restart = false;
returnCode = await RunAsync(settings, runOnce);
returnCode = await RunAsync(settings, runOnce, returnRunOnceJobResult);
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
{

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.");
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);
}
}
}

View File

@@ -295,13 +295,13 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
var runOnceJobCompleted = new TaskCompletionSource<bool>();
var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
_jobDispatcher.Setup(x => x.RunOnceJobCompleted)
.Returns(runOnceJobCompleted);
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
.Callback(() =>
{
runOnceJobCompleted.TrySetResult(true);
runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
});
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
@@ -399,13 +399,13 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
var runOnceJobCompleted = new TaskCompletionSource<bool>();
var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
_jobDispatcher.Setup(x => x.RunOnceJobCompleted)
.Returns(runOnceJobCompleted);
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
.Callback(() =>
{
runOnceJobCompleted.TrySetResult(true);
runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
});
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
@@ -733,8 +733,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(TaskResult.Succeeded);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
@@ -834,8 +834,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(TaskResult.Succeeded);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
@@ -954,8 +954,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(TaskResult.Succeeded);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act