Compare commits

..

6 Commits

Author SHA1 Message Date
dependabot[bot]
a54d973ed5 Bump System.Text.Encoding.CodePages from 8.0.0 to 10.0.3
---
updated-dependencies:
- dependency-name: System.Text.Encoding.CodePages
  dependency-version: 10.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 04:58:44 +00:00
eric sciple
d5a8a936c1 Add telemetry tracking for deprecated set-output and save-state commands (#4221) 2026-02-10 12:28:42 -06:00
Tingluo Huang
cdb77c6804 Support return job result as exitcode in hosted runner. (#4233) 2026-02-10 09:31:10 -05:00
Nikola Jokic
a4a19b152e Bump hook to 0.8.1 (#4222) 2026-02-10 01:07:20 +00:00
Tingluo Huang
1b5486aa8f Validate work dir during runner start up. (#4227) 2026-02-09 08:42:07 -05:00
Takuma Ishikawa
4214709d1b Add support for libssl3 and libssl3t64 for newer Debian/Ubuntu versions (#4213) 2026-02-08 16:03:41 -05:00
11 changed files with 126 additions and 100 deletions

View File

@@ -28,8 +28,8 @@ Debian based OS (Debian, Ubuntu, Linux Mint)
- liblttng-ust1 or liblttng-ust0
- libkrb5-3
- zlib1g
- libssl1.1, libssl1.0.2 or libssl1.0.0
- libicu63, libicu60, libicu57 or libicu55
- libssl3t64, libssl3, libssl1.1, libssl1.0.2 or libssl1.0.0
- libicu76, libicu75, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)

View File

@@ -21,7 +21,7 @@ RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-c
&& unzip ./runner-container-hooks.zip -d ./k8s \
&& rm runner-container-hooks.zip
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.0/actions-runner-hooks-k8s-0.8.0.zip \
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.1/actions-runner-hooks-k8s-0.8.1.zip \
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
&& rm runner-container-hooks.zip

View File

@@ -102,7 +102,7 @@ then
exit 1
fi
apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$
apt_get_with_fallbacks libssl3t64$ libssl3$ libssl1.1$ libssl1.0.2$ libssl1.0.0$
if [ $? -ne 0 ]
then
echo "'$apt_get' failed with exit code '$?'"

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

@@ -15,7 +15,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.3" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
</ItemGroup>

View File

@@ -318,6 +318,17 @@ namespace GitHub.Runner.Worker
context.AddIssue(issue, ExecutionContextLogOptions.Default);
}
if (!context.Global.HasDeprecatedSetOutput)
{
context.Global.HasDeprecatedSetOutput = true;
var telemetry = new JobTelemetry
{
Type = JobTelemetryType.ActionCommand,
Message = "DeprecatedCommand: set-output"
};
context.Global.JobTelemetry.Add(telemetry);
}
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
{
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
@@ -353,6 +364,17 @@ namespace GitHub.Runner.Worker
context.AddIssue(issue, ExecutionContextLogOptions.Default);
}
if (!context.Global.HasDeprecatedSaveState)
{
context.Global.HasDeprecatedSaveState = true;
var telemetry = new JobTelemetry
{
Type = JobTelemetryType.ActionCommand,
Message = "DeprecatedCommand: save-state"
};
context.Global.JobTelemetry.Add(telemetry);
}
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
{
throw new Exception("Required field 'name' is missing in ##[save-state] command.");

View File

@@ -31,5 +31,7 @@ namespace GitHub.Runner.Worker
public JObject ContainerHookState { get; set; }
public bool HasTemplateEvaluatorMismatch { get; set; }
public bool HasActionManifestMismatch { get; set; }
public bool HasDeprecatedSetOutput { get; set; }
public bool HasDeprecatedSaveState { get; set; }
}
}

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
@@ -1076,75 +1076,5 @@ namespace GitHub.Runner.Common.Tests.Listener
Assert.True(hc.AllowAuthMigration);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task RunCommand_ShouldReturnTerminatedError_WhenWorkDirCreationFails()
{
using (var hostCtx = new TestHostContext(this))
{
// Setup: arrange mocks and runner instance
var runnerInstance = new Runner.Listener.Runner();
hostCtx.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hostCtx.SetSingleton<IJobNotification>(_jobNotification.Object);
hostCtx.SetSingleton<IMessageListener>(_messageListener.Object);
hostCtx.SetSingleton<IPromptManager>(_promptManager.Object);
hostCtx.SetSingleton<IConfigurationStore>(_configStore.Object);
hostCtx.SetSingleton<IRunnerServer>(_runnerServer.Object);
hostCtx.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
runnerInstance.Initialize(hostCtx);
// Create a file at the work directory path to block directory creation
string workPath = hostCtx.GetDirectory(WellKnownDirectory.Work);
// Clean up any existing directory first
if (Directory.Exists(workPath))
{
Directory.Delete(workPath, recursive: true);
}
// Place a file where the work directory should be - this blocks Directory.CreateDirectory
string parentPath = Path.GetDirectoryName(workPath);
Assert.NotNull(parentPath);
Assert.NotEmpty(parentPath);
Directory.CreateDirectory(parentPath);
const string blockingFileContent = "test file blocking directory creation";
File.WriteAllText(workPath, blockingFileContent);
const int testPoolId = 12345;
const int testAgentId = 67890;
var runnerConfig = new RunnerSettings
{
PoolId = testPoolId,
AgentId = testAgentId
};
_configurationManager.Setup(m => m.LoadSettings()).Returns(runnerConfig);
_configurationManager.Setup(m => m.IsConfigured()).Returns(true);
_configStore.Setup(m => m.IsServiceConfigured()).Returns(false);
try
{
// Execute: run the command which should fail during work dir validation
var cmd = new CommandSettings(hostCtx, new string[] { "run" });
int exitCode = await runnerInstance.ExecuteCommand(cmd);
// Verify: should return TerminatedError code when dir creation fails
Assert.Equal(Constants.Runner.ReturnCode.TerminatedError, exitCode);
}
finally
{
// Cleanup: remove the blocking file
if (File.Exists(workPath))
{
File.Delete(workPath);
}
}
}
}
}
}

View File

@@ -457,6 +457,8 @@ namespace GitHub.Runner.Common.Tests.Worker
new SetEnvCommandExtension(),
new WarningCommandExtension(),
new AddMaskCommandExtension(),
new SetOutputCommandExtension(),
new SaveStateCommandExtension(),
};
foreach (var command in commands)
{
@@ -499,5 +501,53 @@ namespace GitHub.Runner.Common.Tests.Worker
};
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputCommand_EmitsTelemetryOnce()
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Object.Global.JobTelemetry = new List<JobTelemetry>();
var reference = string.Empty;
_ec.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference));
// First set-output should add telemetry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::set-output name=foo::bar", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
Assert.Equal(JobTelemetryType.ActionCommand, _ec.Object.Global.JobTelemetry[0].Type);
Assert.Equal("DeprecatedCommand: set-output", _ec.Object.Global.JobTelemetry[0].Message);
Assert.True(_ec.Object.Global.HasDeprecatedSetOutput);
// Second set-output should not add another telemetry entry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::set-output name=foo2::bar2", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateCommand_EmitsTelemetryOnce()
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Object.Global.JobTelemetry = new List<JobTelemetry>();
_ec.Setup(x => x.IsEmbedded).Returns(false);
_ec.Setup(x => x.IntraActionState).Returns(new Dictionary<string, string>());
// First save-state should add telemetry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::save-state name=foo::bar", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
Assert.Equal(JobTelemetryType.ActionCommand, _ec.Object.Global.JobTelemetry[0].Type);
Assert.Equal("DeprecatedCommand: save-state", _ec.Object.Global.JobTelemetry[0].Message);
Assert.True(_ec.Object.Global.HasDeprecatedSaveState);
// Second save-state should not add another telemetry entry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::save-state name=foo2::bar2", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
}
}
}
}