mirror of
https://github.com/actions/runner.git
synced 2026-02-18 12:11:11 +08:00
Compare commits
18 Commits
v2.331.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c55ebc454 | ||
|
|
a798a45826 | ||
|
|
9efea31a89 | ||
|
|
6680090084 | ||
|
|
15cb558d8f | ||
|
|
d5a8a936c1 | ||
|
|
cdb77c6804 | ||
|
|
a4a19b152e | ||
|
|
1b5486aa8f | ||
|
|
4214709d1b | ||
|
|
3ffedabea3 | ||
|
|
3a80a78cae | ||
|
|
6822f4aba2 | ||
|
|
ad43c639cf | ||
|
|
5d4fb30d5b | ||
|
|
1df72a54ca | ||
|
|
02013cf967 | ||
|
|
7d5c17a190 |
@@ -4,7 +4,7 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
||||||
"ghcr.io/devcontainers/features/dotnet": {
|
"ghcr.io/devcontainers/features/dotnet": {
|
||||||
"version": "8.0.416"
|
"version": "8.0.418"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20"
|
"version": "20"
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ Debian based OS (Debian, Ubuntu, Linux Mint)
|
|||||||
- liblttng-ust1 or liblttng-ust0
|
- liblttng-ust1 or liblttng-ust0
|
||||||
- libkrb5-3
|
- libkrb5-3
|
||||||
- zlib1g
|
- zlib1g
|
||||||
- libssl1.1, libssl1.0.2 or libssl1.0.0
|
- libssl3t64, libssl3, libssl1.1, libssl1.0.2 or libssl1.0.0
|
||||||
- libicu63, libicu60, libicu57 or libicu55
|
- libicu76, libicu75, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
|
||||||
|
|
||||||
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||||
ARG DOCKER_VERSION=29.0.2
|
ARG DOCKER_VERSION=29.2.0
|
||||||
ARG BUILDX_VERSION=0.30.1
|
ARG BUILDX_VERSION=0.31.1
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
RUN apt update -y && apt install curl unzip -y
|
||||||
|
|
||||||
@@ -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 \
|
&& unzip ./runner-container-hooks.zip -d ./k8s \
|
||||||
&& rm runner-container-hooks.zip
|
&& 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 \
|
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
|
||||||
&& rm runner-container-hooks.zip
|
&& rm runner-container-hooks.zip
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ NODE_URL=https://nodejs.org/dist
|
|||||||
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
||||||
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
||||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||||
NODE20_VERSION="20.19.6"
|
NODE20_VERSION="20.20.0"
|
||||||
NODE24_VERSION="24.12.0"
|
NODE24_VERSION="24.13.1"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
# exploits the fact that pwd will print abs path when no args
|
# exploits the fact that pwd will print abs path when no args
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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 ]
|
if [ $? -ne 0 ]
|
||||||
then
|
then
|
||||||
echo "'$apt_get' failed with exit code '$?'"
|
echo "'$apt_get' failed with exit code '$?'"
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
|
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
|
||||||
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
|
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
|
||||||
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
|
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
|
||||||
|
public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node version migration related constants
|
// Node version migration related constants
|
||||||
|
|||||||
@@ -178,8 +178,12 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate can connect.
|
// Validate can connect using the obtained vss credentials.
|
||||||
|
// In Runner Admin flow there's nothing new to test connection to at this point as registerToken is already validated via GetTenantCredential.
|
||||||
|
if (!runnerSettings.UseRunnerAdminFlow)
|
||||||
|
{
|
||||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), creds);
|
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), creds);
|
||||||
|
}
|
||||||
|
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
_term.WriteSuccessMessage("Connected to GitHub");
|
_term.WriteSuccessMessage("Connected to GitHub");
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -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,12 +404,27 @@ 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
|
||||||
{
|
{
|
||||||
Trace.Info(nameof(RunAsync));
|
Trace.Info(nameof(RunAsync));
|
||||||
|
|
||||||
|
// Validate directory permissions.
|
||||||
|
string workDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
|
||||||
|
Trace.Info($"Validating directory permissions for: '{workDirectory}'");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(workDirectory);
|
||||||
|
IOUtil.ValidateExecutePermission(workDirectory);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error(ex);
|
||||||
|
_term.WriteError($"Fail to create and validate runner's work directory '{workDirectory}'.");
|
||||||
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
|
}
|
||||||
|
|
||||||
// First try using migrated settings if available
|
// First try using migrated settings if available
|
||||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
RunnerSettings migratedSettings = null;
|
RunnerSettings migratedSettings = null;
|
||||||
@@ -565,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -851,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -318,6 +318,17 @@ namespace GitHub.Runner.Worker
|
|||||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
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))
|
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
|
||||||
{
|
{
|
||||||
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
|
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);
|
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))
|
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
|
||||||
{
|
{
|
||||||
throw new Exception("Required field 'name' is missing in ##[save-state] command.");
|
throw new Exception("Required field 'name' is missing in ##[save-state] command.");
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ namespace GitHub.Runner.Worker
|
|||||||
"EvaluateContainerEnvironment",
|
"EvaluateContainerEnvironment",
|
||||||
() => _legacyManager.EvaluateContainerEnvironment(executionContext, token, extraExpressionValues),
|
() => _legacyManager.EvaluateContainerEnvironment(executionContext, token, extraExpressionValues),
|
||||||
() => _newManager.EvaluateContainerEnvironment(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.MappingToken, ConvertToNewExpressionValues(extraExpressionValues)),
|
() => _newManager.EvaluateContainerEnvironment(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.MappingToken, ConvertToNewExpressionValues(extraExpressionValues)),
|
||||||
(legacyResult, newResult) => {
|
(legacyResult, newResult) =>
|
||||||
|
{
|
||||||
var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper));
|
var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper));
|
||||||
return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment");
|
return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment");
|
||||||
});
|
});
|
||||||
@@ -165,9 +166,150 @@ namespace GitHub.Runner.Worker
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize new steps and deserialize to old steps
|
var result = new List<GitHub.DistributedTask.Pipelines.ActionStep>();
|
||||||
var json = StringUtil.ConvertToJson(newSteps, Newtonsoft.Json.Formatting.None);
|
foreach (var step in newSteps)
|
||||||
return StringUtil.ConvertFromJson<List<GitHub.DistributedTask.Pipelines.ActionStep>>(json);
|
{
|
||||||
|
var actionStep = new GitHub.DistributedTask.Pipelines.ActionStep
|
||||||
|
{
|
||||||
|
ContextName = step.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step is GitHub.Actions.WorkflowParser.RunStep runStep)
|
||||||
|
{
|
||||||
|
actionStep.Condition = ExtractConditionString(runStep.If);
|
||||||
|
actionStep.DisplayNameToken = ConvertToLegacyToken<TemplateToken>(runStep.Name);
|
||||||
|
actionStep.ContinueOnError = ConvertToLegacyToken<TemplateToken>(runStep.ContinueOnError);
|
||||||
|
actionStep.TimeoutInMinutes = ConvertToLegacyToken<TemplateToken>(runStep.TimeoutMinutes);
|
||||||
|
actionStep.Environment = ConvertToLegacyToken<TemplateToken>(runStep.Env);
|
||||||
|
actionStep.Reference = new GitHub.DistributedTask.Pipelines.ScriptReference();
|
||||||
|
actionStep.Inputs = BuildRunStepInputs(runStep);
|
||||||
|
}
|
||||||
|
else if (step is GitHub.Actions.WorkflowParser.ActionStep usesStep)
|
||||||
|
{
|
||||||
|
actionStep.Condition = ExtractConditionString(usesStep.If);
|
||||||
|
actionStep.DisplayNameToken = ConvertToLegacyToken<TemplateToken>(usesStep.Name);
|
||||||
|
actionStep.ContinueOnError = ConvertToLegacyToken<TemplateToken>(usesStep.ContinueOnError);
|
||||||
|
actionStep.TimeoutInMinutes = ConvertToLegacyToken<TemplateToken>(usesStep.TimeoutMinutes);
|
||||||
|
actionStep.Environment = ConvertToLegacyToken<TemplateToken>(usesStep.Env);
|
||||||
|
actionStep.Reference = ParseActionReference(usesStep.Uses?.Value);
|
||||||
|
actionStep.Inputs = ConvertToLegacyToken<MappingToken>(usesStep.With);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(actionStep);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractConditionString(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.BasicExpressionToken ifToken)
|
||||||
|
{
|
||||||
|
if (ifToken == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Expression property is internal, so we use ToString() which formats as "${{ expr }}"
|
||||||
|
// Then strip the delimiters to get just the expression
|
||||||
|
var str = ifToken.ToString();
|
||||||
|
if (str.StartsWith("${{") && str.EndsWith("}}"))
|
||||||
|
{
|
||||||
|
return str.Substring(3, str.Length - 5).Trim();
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MappingToken BuildRunStepInputs(GitHub.Actions.WorkflowParser.RunStep runStep)
|
||||||
|
{
|
||||||
|
var inputs = new MappingToken(null, null, null);
|
||||||
|
|
||||||
|
// script (from run)
|
||||||
|
if (runStep.Run != null)
|
||||||
|
{
|
||||||
|
inputs.Add(
|
||||||
|
new StringToken(null, null, null, "script"),
|
||||||
|
ConvertToLegacyToken<TemplateToken>(runStep.Run));
|
||||||
|
}
|
||||||
|
|
||||||
|
// shell
|
||||||
|
if (runStep.Shell != null)
|
||||||
|
{
|
||||||
|
inputs.Add(
|
||||||
|
new StringToken(null, null, null, "shell"),
|
||||||
|
ConvertToLegacyToken<TemplateToken>(runStep.Shell));
|
||||||
|
}
|
||||||
|
|
||||||
|
// working-directory
|
||||||
|
if (runStep.WorkingDirectory != null)
|
||||||
|
{
|
||||||
|
inputs.Add(
|
||||||
|
new StringToken(null, null, null, "workingDirectory"),
|
||||||
|
ConvertToLegacyToken<TemplateToken>(runStep.WorkingDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs.Count > 0 ? inputs : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GitHub.DistributedTask.Pipelines.ActionStepDefinitionReference ParseActionReference(string uses)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uses))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker reference: docker://image:tag
|
||||||
|
if (uses.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new GitHub.DistributedTask.Pipelines.ContainerRegistryReference
|
||||||
|
{
|
||||||
|
Image = uses.Substring("docker://".Length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local path reference: ./path/to/action
|
||||||
|
if (uses.StartsWith("./") || uses.StartsWith(".\\"))
|
||||||
|
{
|
||||||
|
return new GitHub.DistributedTask.Pipelines.RepositoryPathReference
|
||||||
|
{
|
||||||
|
RepositoryType = "self",
|
||||||
|
Path = uses
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository reference: owner/repo@ref or owner/repo/path@ref
|
||||||
|
var atIndex = uses.LastIndexOf('@');
|
||||||
|
string refPart = null;
|
||||||
|
string repoPart = uses;
|
||||||
|
|
||||||
|
if (atIndex > 0)
|
||||||
|
{
|
||||||
|
refPart = uses.Substring(atIndex + 1);
|
||||||
|
repoPart = uses.Substring(0, atIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by / to get owner/repo and optional path
|
||||||
|
var parts = repoPart.Split('/');
|
||||||
|
string name;
|
||||||
|
string path = null;
|
||||||
|
|
||||||
|
if (parts.Length >= 2)
|
||||||
|
{
|
||||||
|
name = $"{parts[0]}/{parts[1]}";
|
||||||
|
if (parts.Length > 2)
|
||||||
|
{
|
||||||
|
path = string.Join("/", parts, 2, parts.Length - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
name = repoPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GitHub.DistributedTask.Pipelines.RepositoryPathReference
|
||||||
|
{
|
||||||
|
RepositoryType = "GitHub",
|
||||||
|
Name = name,
|
||||||
|
Ref = refPart,
|
||||||
|
Path = path
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private T ConvertToLegacyToken<T>(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken newToken) where T : TemplateToken
|
private T ConvertToLegacyToken<T>(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken newToken) where T : TemplateToken
|
||||||
@@ -633,6 +775,14 @@ namespace GitHub.Runner.Worker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for known equivalent error patterns (e.g., JSON parse errors)
|
||||||
|
// where both parsers correctly reject invalid input but with different wording
|
||||||
|
if (PipelineTemplateEvaluatorWrapper.HasJsonExceptionType(legacyException) && PipelineTemplateEvaluatorWrapper.HasJsonExceptionType(newException))
|
||||||
|
{
|
||||||
|
trace.Info("CompareExceptions - both exceptions are JSON parse errors, treating as matched");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Compare exception messages recursively (including inner exceptions)
|
// Compare exception messages recursively (including inner exceptions)
|
||||||
var legacyMessages = GetExceptionMessages(legacyException);
|
var legacyMessages = GetExceptionMessages(legacyException);
|
||||||
var newMessages = GetExceptionMessages(newException);
|
var newMessages = GetExceptionMessages(newException);
|
||||||
@@ -697,5 +847,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,7 +379,14 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
||||||
var repositoryReference = action.Reference as RepositoryPathReference;
|
var repositoryReference = action.Reference as RepositoryPathReference;
|
||||||
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
|
var pathString = string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(repositoryReference.Path))
|
||||||
|
{
|
||||||
|
// For local actions (Name is empty), don't prepend "/" to avoid "/./"
|
||||||
|
pathString = string.IsNullOrEmpty(repositoryReference.Name)
|
||||||
|
? repositoryReference.Path
|
||||||
|
: $"/{repositoryReference.Path}";
|
||||||
|
}
|
||||||
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
||||||
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
||||||
tokenToParse = new StringToken(null, null, null, repoString);
|
tokenToParse = new StringToken(null, null, null, repoString);
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
PublishStepTelemetry();
|
PublishStepTelemetry();
|
||||||
|
|
||||||
if (_record.RecordType == "Task")
|
if (_record.RecordType == ExecutionContextType.Task)
|
||||||
{
|
{
|
||||||
var stepResult = new StepResult
|
var stepResult = new StepResult
|
||||||
{
|
{
|
||||||
@@ -532,6 +532,25 @@ namespace GitHub.Runner.Worker
|
|||||||
Global.StepsResult.Add(stepResult);
|
Global.StepsResult.Add(stepResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Global.Variables.GetBoolean(Constants.Runner.Features.SendJobLevelAnnotations) ?? false)
|
||||||
|
{
|
||||||
|
if (_record.RecordType == ExecutionContextType.Job)
|
||||||
|
{
|
||||||
|
_record.Issues?.ForEach(issue =>
|
||||||
|
{
|
||||||
|
var annotation = issue.ToAnnotation();
|
||||||
|
if (annotation != null)
|
||||||
|
{
|
||||||
|
Global.JobAnnotations.Add(annotation.Value);
|
||||||
|
if (annotation.Value.IsInfrastructureIssue && string.IsNullOrEmpty(Global.InfrastructureFailureCategory))
|
||||||
|
{
|
||||||
|
Global.InfrastructureFailureCategory = issue.Category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Root != this)
|
if (Root != this)
|
||||||
{
|
{
|
||||||
// only dispose TokenSource for step level ExecutionContext
|
// only dispose TokenSource for step level ExecutionContext
|
||||||
|
|||||||
@@ -31,5 +31,7 @@ namespace GitHub.Runner.Worker
|
|||||||
public JObject ContainerHookState { get; set; }
|
public JObject ContainerHookState { get; set; }
|
||||||
public bool HasTemplateEvaluatorMismatch { get; set; }
|
public bool HasTemplateEvaluatorMismatch { get; set; }
|
||||||
public bool HasActionManifestMismatch { get; set; }
|
public bool HasActionManifestMismatch { get; set; }
|
||||||
|
public bool HasDeprecatedSetOutput { get; set; }
|
||||||
|
public bool HasDeprecatedSaveState { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/Runner.Worker/InternalsVisibleTo.cs
Normal file
3
src/Runner.Worker/InternalsVisibleTo.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Test")]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GitHub.Actions.WorkflowParser;
|
using GitHub.Actions.WorkflowParser;
|
||||||
using GitHub.DistributedTask.Expressions2;
|
using GitHub.DistributedTask.Expressions2;
|
||||||
@@ -216,12 +216,15 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
internal TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
||||||
string methodName,
|
string methodName,
|
||||||
Func<TLegacy> legacyEvaluator,
|
Func<TLegacy> legacyEvaluator,
|
||||||
Func<TNew> newEvaluator,
|
Func<TNew> newEvaluator,
|
||||||
Func<TLegacy, TNew, bool> resultComparer)
|
Func<TLegacy, TNew, bool> resultComparer)
|
||||||
{
|
{
|
||||||
|
// Capture cancellation state before evaluation
|
||||||
|
var cancellationRequestedBefore = _context.CancellationToken.IsCancellationRequested;
|
||||||
|
|
||||||
// Legacy evaluator
|
// Legacy evaluator
|
||||||
var legacyException = default(Exception);
|
var legacyException = default(Exception);
|
||||||
var legacyResult = default(TLegacy);
|
var legacyResult = default(TLegacy);
|
||||||
@@ -253,14 +256,18 @@ namespace GitHub.Runner.Worker
|
|||||||
newException = ex;
|
newException = ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture cancellation state after evaluation
|
||||||
|
var cancellationRequestedAfter = _context.CancellationToken.IsCancellationRequested;
|
||||||
|
|
||||||
// Compare results or exceptions
|
// Compare results or exceptions
|
||||||
|
bool hasMismatch = false;
|
||||||
if (legacyException != null || newException != null)
|
if (legacyException != null || newException != null)
|
||||||
{
|
{
|
||||||
// Either one or both threw exceptions - compare them
|
// Either one or both threw exceptions - compare them
|
||||||
if (!CompareExceptions(legacyException, newException))
|
if (!CompareExceptions(legacyException, newException))
|
||||||
{
|
{
|
||||||
_trace.Info($"{methodName} exception mismatch");
|
_trace.Info($"{methodName} exception mismatch");
|
||||||
RecordMismatch($"{methodName}");
|
hasMismatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -269,6 +276,20 @@ namespace GitHub.Runner.Worker
|
|||||||
if (!resultComparer(legacyResult, newResult))
|
if (!resultComparer(legacyResult, newResult))
|
||||||
{
|
{
|
||||||
_trace.Info($"{methodName} mismatch");
|
_trace.Info($"{methodName} mismatch");
|
||||||
|
hasMismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only record mismatch if it wasn't caused by a cancellation race condition
|
||||||
|
if (hasMismatch)
|
||||||
|
{
|
||||||
|
if (!cancellationRequestedBefore && cancellationRequestedAfter)
|
||||||
|
{
|
||||||
|
// Cancellation state changed during evaluation window - skip recording
|
||||||
|
_trace.Info($"{methodName} mismatch skipped due to cancellation race condition");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
RecordMismatch($"{methodName}");
|
RecordMismatch($"{methodName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,6 +633,13 @@ namespace GitHub.Runner.Worker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for known equivalent error patterns (e.g., JSON parse errors)
|
||||||
|
// where both parsers correctly reject invalid input but with different wording
|
||||||
|
if (IsKnownEquivalentErrorPattern(legacyException, newException))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Compare exception messages recursively (including inner exceptions)
|
// Compare exception messages recursively (including inner exceptions)
|
||||||
var legacyMessages = GetExceptionMessages(legacyException);
|
var legacyMessages = GetExceptionMessages(legacyException);
|
||||||
var newMessages = GetExceptionMessages(newException);
|
var newMessages = GetExceptionMessages(newException);
|
||||||
@@ -634,6 +662,67 @@ namespace GitHub.Runner.Worker
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if two exceptions match a known pattern where both parsers correctly reject
|
||||||
|
/// invalid input but with different error messages (e.g., JSON parse errors from fromJSON).
|
||||||
|
/// </summary>
|
||||||
|
private bool IsKnownEquivalentErrorPattern(Exception legacyException, Exception newException)
|
||||||
|
{
|
||||||
|
// fromJSON('') - both parsers fail when parsing empty string as JSON
|
||||||
|
// The error messages differ but both indicate JSON parsing failure.
|
||||||
|
// Legacy throws raw JsonReaderException: "Error reading JToken from JsonReader..."
|
||||||
|
// New wraps it: "Error parsing fromJson" with inner JsonReaderException
|
||||||
|
// Both may be wrapped in TemplateValidationException: "The template is not valid..."
|
||||||
|
if (HasJsonExceptionType(legacyException) && HasJsonExceptionType(newException))
|
||||||
|
{
|
||||||
|
_trace.Info("CompareExceptions - both exceptions are JSON parse errors, treating as matched");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the exception chain contains a JSON-related exception type.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool HasJsonExceptionType(Exception ex)
|
||||||
|
{
|
||||||
|
var toProcess = new Queue<Exception>();
|
||||||
|
toProcess.Enqueue(ex);
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
while (toProcess.Count > 0 && count < 50)
|
||||||
|
{
|
||||||
|
var current = toProcess.Dequeue();
|
||||||
|
if (current == null) continue;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (current is Newtonsoft.Json.JsonReaderException ||
|
||||||
|
current is System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current is AggregateException aggregateEx)
|
||||||
|
{
|
||||||
|
foreach (var innerEx in aggregateEx.InnerExceptions)
|
||||||
|
{
|
||||||
|
if (innerEx != null && count < 50)
|
||||||
|
{
|
||||||
|
toProcess.Enqueue(innerEx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (current.InnerException != null)
|
||||||
|
{
|
||||||
|
toProcess.Enqueue(current.InnerException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private IList<string> GetExceptionMessages(Exception ex)
|
private IList<string> GetExceptionMessages(Exception ex)
|
||||||
{
|
{
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
|
|||||||
@@ -421,7 +421,7 @@
|
|||||||
"mapping": {
|
"mapping": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"image": "string",
|
"image": "string",
|
||||||
"options": "non-empty-string",
|
"options": "string",
|
||||||
"env": "container-env",
|
"env": "container-env",
|
||||||
"ports": "sequence-of-non-empty-string",
|
"ports": "sequence-of-non-empty-string",
|
||||||
"volumes": "sequence-of-non-empty-string",
|
"volumes": "sequence-of-non-empty-string",
|
||||||
|
|||||||
@@ -23,14 +23,14 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
|
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.2" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
<PackageReference Include="Minimatch" Version="2.0.0" />
|
<PackageReference Include="Minimatch" Version="2.0.0" />
|
||||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||||
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
|
<PackageReference Include="System.Formats.Asn1" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
|
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -1153,8 +1153,13 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(result.Image))
|
if (String.IsNullOrEmpty(result.Image))
|
||||||
|
{
|
||||||
|
// Only error during early validation (parse time)
|
||||||
|
// At runtime (expression evaluation), empty image = no container
|
||||||
|
if (isEarlyValidation)
|
||||||
{
|
{
|
||||||
context.Error(value, "Container image cannot be empty");
|
context.Error(value, "Container image cannot be empty");
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2593,7 +2593,7 @@
|
|||||||
"description": "Use `jobs.<job_id>.container.image` to define the Docker image to use as the container to run the action. The value can be the Docker Hub image or a registry name."
|
"description": "Use `jobs.<job_id>.container.image` to define the Docker image to use as the container to run the action. The value can be the Docker Hub image or a registry name."
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"type": "non-empty-string",
|
"type": "string",
|
||||||
"description": "Use `jobs.<job_id>.container.options` to configure additional Docker container resource options."
|
"description": "Use `jobs.<job_id>.container.options` to configure additional Docker container resource options."
|
||||||
},
|
},
|
||||||
"env": "container-env",
|
"env": "container-env",
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -457,6 +457,8 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
new SetEnvCommandExtension(),
|
new SetEnvCommandExtension(),
|
||||||
new WarningCommandExtension(),
|
new WarningCommandExtension(),
|
||||||
new AddMaskCommandExtension(),
|
new AddMaskCommandExtension(),
|
||||||
|
new SetOutputCommandExtension(),
|
||||||
|
new SaveStateCommandExtension(),
|
||||||
};
|
};
|
||||||
foreach (var command in commands)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
424
src/Test/L0/Worker/ActionManifestParserComparisonL0.cs
Normal file
424
src/Test/L0/Worker/ActionManifestParserComparisonL0.cs
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
using GitHub.Actions.WorkflowParser;
|
||||||
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker;
|
||||||
|
using LegacyContextData = GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using LegacyExpressions = GitHub.DistributedTask.Expressions2;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Tests.Worker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for parser comparison wrapper classes.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ActionManifestParserComparisonL0
|
||||||
|
{
|
||||||
|
private CancellationTokenSource _ecTokenSource;
|
||||||
|
private Mock<IExecutionContext> _ec;
|
||||||
|
private TestHostContext _hc;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void ConvertToLegacySteps_ProducesCorrectSteps_WithExplicitPropertyMapping()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange - Test that ActionManifestManagerWrapper properly converts new steps to legacy format
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
// Enable comparison feature
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
// Register required services
|
||||||
|
var legacyManager = new ActionManifestManagerLegacy();
|
||||||
|
legacyManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
||||||
|
|
||||||
|
var newManager = new ActionManifestManager();
|
||||||
|
newManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
||||||
|
|
||||||
|
var wrapper = new ActionManifestManagerWrapper();
|
||||||
|
wrapper.Initialize(_hc);
|
||||||
|
|
||||||
|
var manifestPath = Path.Combine(TestUtil.GetTestDataPath(), "conditional_composite_action.yml");
|
||||||
|
|
||||||
|
// Act - Load through the wrapper (which internally converts)
|
||||||
|
var result = wrapper.Load(_ec.Object, manifestPath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType);
|
||||||
|
|
||||||
|
var compositeExecution = result.Execution as CompositeActionExecutionData;
|
||||||
|
Assert.NotNull(compositeExecution);
|
||||||
|
Assert.NotNull(compositeExecution.Steps);
|
||||||
|
Assert.Equal(6, compositeExecution.Steps.Count);
|
||||||
|
|
||||||
|
// Verify steps are NOT null (this was the bug - JSON round-trip produced nulls)
|
||||||
|
foreach (var step in compositeExecution.Steps)
|
||||||
|
{
|
||||||
|
Assert.NotNull(step);
|
||||||
|
Assert.NotNull(step.Reference);
|
||||||
|
Assert.IsType<GitHub.DistributedTask.Pipelines.ScriptReference>(step.Reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify step with condition
|
||||||
|
var successStep = compositeExecution.Steps[2];
|
||||||
|
Assert.Equal("success-conditional", successStep.ContextName);
|
||||||
|
Assert.Equal("success()", successStep.Condition);
|
||||||
|
|
||||||
|
// Verify step with complex condition
|
||||||
|
var lastStep = compositeExecution.Steps[5];
|
||||||
|
Assert.Contains("inputs.exit-code == 1", lastStep.Condition);
|
||||||
|
Assert.Contains("failure()", lastStep.Condition);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateJobContainer_EmptyImage_BothParsersReturnNull()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange - Test that both parsers return null for empty container image at runtime
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
var fileTable = new List<string>();
|
||||||
|
|
||||||
|
// Create legacy evaluator
|
||||||
|
var legacyTraceWriter = new GitHub.DistributedTask.ObjectTemplating.EmptyTraceWriter();
|
||||||
|
var schema = PipelineTemplateSchemaFactory.GetSchema();
|
||||||
|
var legacyEvaluator = new PipelineTemplateEvaluator(legacyTraceWriter, schema, fileTable);
|
||||||
|
|
||||||
|
// Create new evaluator
|
||||||
|
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
||||||
|
var newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, fileTable, features: null);
|
||||||
|
|
||||||
|
// Create a token representing an empty container image (simulates expression evaluated to empty string)
|
||||||
|
var emptyImageToken = new StringToken(null, null, null, "");
|
||||||
|
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
// Act - Call both evaluators
|
||||||
|
var legacyResult = legacyEvaluator.EvaluateJobContainer(emptyImageToken, contextData, expressionFunctions);
|
||||||
|
|
||||||
|
// Convert token for new evaluator
|
||||||
|
var newToken = new GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.StringToken(null, null, null, "");
|
||||||
|
var newContextData = new GitHub.Actions.Expressions.Data.DictionaryExpressionData();
|
||||||
|
var newExpressionFunctions = new List<GitHub.Actions.Expressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var newResult = newEvaluator.EvaluateJobContainer(newToken, newContextData, newExpressionFunctions);
|
||||||
|
|
||||||
|
// Assert - Both should return null for empty image (no container)
|
||||||
|
Assert.Null(legacyResult);
|
||||||
|
Assert.Null(newResult);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void FromJsonEmptyString_BothParsersFail_WithDifferentMessages()
|
||||||
|
{
|
||||||
|
// This test verifies that both parsers fail with different error messages when parsing fromJSON('')
|
||||||
|
// The comparison layer should treat these as semantically equivalent (both are JSON parse errors)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
var fileTable = new List<string>();
|
||||||
|
|
||||||
|
// Create legacy evaluator
|
||||||
|
var legacyTraceWriter = new GitHub.DistributedTask.ObjectTemplating.EmptyTraceWriter();
|
||||||
|
var schema = PipelineTemplateSchemaFactory.GetSchema();
|
||||||
|
var legacyEvaluator = new PipelineTemplateEvaluator(legacyTraceWriter, schema, fileTable);
|
||||||
|
|
||||||
|
// Create new evaluator
|
||||||
|
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
||||||
|
var newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, fileTable, features: null);
|
||||||
|
|
||||||
|
// Create expression token for fromJSON('')
|
||||||
|
var legacyToken = new BasicExpressionToken(null, null, null, "fromJson('')");
|
||||||
|
var newToken = new GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.BasicExpressionToken(null, null, null, "fromJson('')");
|
||||||
|
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var newContextData = new GitHub.Actions.Expressions.Data.DictionaryExpressionData();
|
||||||
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
var newExpressionFunctions = new List<GitHub.Actions.Expressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
// Act - Both should throw
|
||||||
|
Exception legacyException = null;
|
||||||
|
Exception newException = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
legacyEvaluator.EvaluateStepDisplayName(legacyToken, contextData, expressionFunctions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
legacyException = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newEvaluator.EvaluateStepName(newToken, newContextData, newExpressionFunctions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
newException = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert - Both threw exceptions
|
||||||
|
Assert.NotNull(legacyException);
|
||||||
|
Assert.NotNull(newException);
|
||||||
|
|
||||||
|
// Verify the error messages are different (which is why we need semantic comparison)
|
||||||
|
Assert.NotEqual(legacyException.Message, newException.Message);
|
||||||
|
|
||||||
|
// Verify both are JSON parse errors (contain JSON-related error indicators)
|
||||||
|
var legacyFullMsg = GetFullExceptionMessage(legacyException);
|
||||||
|
var newFullMsg = GetFullExceptionMessage(newException);
|
||||||
|
|
||||||
|
// At least one should contain indicators of JSON parsing failure
|
||||||
|
var legacyIsJsonError = legacyFullMsg.Contains("JToken") ||
|
||||||
|
legacyFullMsg.Contains("JsonReader") ||
|
||||||
|
legacyFullMsg.Contains("fromJson");
|
||||||
|
var newIsJsonError = newFullMsg.Contains("JToken") ||
|
||||||
|
newFullMsg.Contains("JsonReader") ||
|
||||||
|
newFullMsg.Contains("fromJson");
|
||||||
|
|
||||||
|
Assert.True(legacyIsJsonError, $"Legacy exception should be JSON error: {legacyFullMsg}");
|
||||||
|
Assert.True(newIsJsonError, $"New exception should be JSON error: {newFullMsg}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateDefaultInput_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var legacyManager = new ActionManifestManagerLegacy();
|
||||||
|
legacyManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
||||||
|
|
||||||
|
var newManager = new ActionManifestManager();
|
||||||
|
newManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
||||||
|
|
||||||
|
var wrapper = new ActionManifestManagerWrapper();
|
||||||
|
wrapper.Initialize(_hc);
|
||||||
|
|
||||||
|
_ec.Object.ExpressionValues["github"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionValues["strategy"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionValues["matrix"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionValues["steps"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionValues["job"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionValues["runner"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionValues["env"] = new LegacyContextData.DictionaryContextData();
|
||||||
|
_ec.Object.ExpressionFunctions.Add(new LegacyExpressions.FunctionInfo<GitHub.Runner.Worker.Expressions.HashFilesFunction>("hashFiles", 1, 255));
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateDefaultInput(_ec.Object, "testInput", new StringToken(null, null, null, "defaultValue"));
|
||||||
|
|
||||||
|
Assert.Equal("defaultValue", result);
|
||||||
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateContainerArguments_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var legacyManager = new ActionManifestManagerLegacy();
|
||||||
|
legacyManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
||||||
|
|
||||||
|
var newManager = new ActionManifestManager();
|
||||||
|
newManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
||||||
|
|
||||||
|
var wrapper = new ActionManifestManagerWrapper();
|
||||||
|
wrapper.Initialize(_hc);
|
||||||
|
|
||||||
|
var arguments = new SequenceToken(null, null, null);
|
||||||
|
arguments.Add(new StringToken(null, null, null, "arg1"));
|
||||||
|
arguments.Add(new StringToken(null, null, null, "arg2"));
|
||||||
|
|
||||||
|
var evaluateContext = new Dictionary<string, LegacyContextData.PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateContainerArguments(_ec.Object, arguments, evaluateContext);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Equal("arg1", result[0]);
|
||||||
|
Assert.Equal("arg2", result[1]);
|
||||||
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateContainerEnvironment_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var legacyManager = new ActionManifestManagerLegacy();
|
||||||
|
legacyManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
||||||
|
|
||||||
|
var newManager = new ActionManifestManager();
|
||||||
|
newManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
||||||
|
|
||||||
|
var wrapper = new ActionManifestManagerWrapper();
|
||||||
|
wrapper.Initialize(_hc);
|
||||||
|
|
||||||
|
var environment = new MappingToken(null, null, null);
|
||||||
|
environment.Add(new StringToken(null, null, null, "hello"), new StringToken(null, null, null, "world"));
|
||||||
|
|
||||||
|
var evaluateContext = new Dictionary<string, LegacyContextData.PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateContainerEnvironment(_ec.Object, environment, evaluateContext);
|
||||||
|
|
||||||
|
Assert.Equal(1, result.Count);
|
||||||
|
Assert.Equal("world", result["hello"]);
|
||||||
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateCompositeOutputs_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var legacyManager = new ActionManifestManagerLegacy();
|
||||||
|
legacyManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
||||||
|
|
||||||
|
var newManager = new ActionManifestManager();
|
||||||
|
newManager.Initialize(_hc);
|
||||||
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
||||||
|
|
||||||
|
var wrapper = new ActionManifestManagerWrapper();
|
||||||
|
wrapper.Initialize(_hc);
|
||||||
|
|
||||||
|
var outputDef = new MappingToken(null, null, null);
|
||||||
|
outputDef.Add(new StringToken(null, null, null, "description"), new StringToken(null, null, null, "test output"));
|
||||||
|
outputDef.Add(new StringToken(null, null, null, "value"), new StringToken(null, null, null, "value1"));
|
||||||
|
|
||||||
|
var token = new MappingToken(null, null, null);
|
||||||
|
token.Add(new StringToken(null, null, null, "output1"), outputDef);
|
||||||
|
|
||||||
|
var evaluateContext = new Dictionary<string, LegacyContextData.PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateCompositeOutputs(_ec.Object, token, evaluateContext);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFullExceptionMessage(Exception ex)
|
||||||
|
{
|
||||||
|
var messages = new List<string>();
|
||||||
|
var current = ex;
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
messages.Add(current.Message);
|
||||||
|
current = current.InnerException;
|
||||||
|
}
|
||||||
|
return string.Join(" -> ", messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Setup([CallerMemberName] string name = "")
|
||||||
|
{
|
||||||
|
_ecTokenSource?.Dispose();
|
||||||
|
_ecTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_hc = new TestHostContext(this, name);
|
||||||
|
|
||||||
|
var expressionValues = new LegacyContextData.DictionaryContextData();
|
||||||
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
_ec = new Mock<IExecutionContext>();
|
||||||
|
_ec.Setup(x => x.Global)
|
||||||
|
.Returns(new GlobalContext
|
||||||
|
{
|
||||||
|
FileTable = new List<String>(),
|
||||||
|
Variables = new Variables(_hc, new Dictionary<string, VariableValue>()),
|
||||||
|
WriteDebug = true,
|
||||||
|
});
|
||||||
|
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
|
||||||
|
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
||||||
|
_ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions);
|
||||||
|
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
|
||||||
|
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Teardown()
|
||||||
|
{
|
||||||
|
_hc?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -316,6 +316,94 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Assert.Equal("${{ matrix.node }}", _actionRunner.DisplayName);
|
Assert.Equal("${{ matrix.node }}", _actionRunner.DisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateDisplayNameForLocalAction()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Setup();
|
||||||
|
var actionId = Guid.NewGuid();
|
||||||
|
var action = new Pipelines.ActionStep()
|
||||||
|
{
|
||||||
|
Name = "action",
|
||||||
|
Id = actionId,
|
||||||
|
Reference = new Pipelines.RepositoryPathReference()
|
||||||
|
{
|
||||||
|
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Path = "./"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_actionRunner.Action = action;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(validDisplayName);
|
||||||
|
Assert.True(updated);
|
||||||
|
Assert.Equal("Run ./", _actionRunner.DisplayName); // NOT "Run /./"
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateDisplayNameForLocalActionWithPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Setup();
|
||||||
|
var actionId = Guid.NewGuid();
|
||||||
|
var action = new Pipelines.ActionStep()
|
||||||
|
{
|
||||||
|
Name = "action",
|
||||||
|
Id = actionId,
|
||||||
|
Reference = new Pipelines.RepositoryPathReference()
|
||||||
|
{
|
||||||
|
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Path = "./.github/actions/my-action"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_actionRunner.Action = action;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(validDisplayName);
|
||||||
|
Assert.True(updated);
|
||||||
|
Assert.Equal("Run ./.github/actions/my-action", _actionRunner.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateDisplayNameForRemoteActionWithPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Setup();
|
||||||
|
var actionId = Guid.NewGuid();
|
||||||
|
var action = new Pipelines.ActionStep()
|
||||||
|
{
|
||||||
|
Name = "action",
|
||||||
|
Id = actionId,
|
||||||
|
Reference = new Pipelines.RepositoryPathReference()
|
||||||
|
{
|
||||||
|
Name = "owner/repo",
|
||||||
|
Path = "subdir",
|
||||||
|
Ref = "v1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_actionRunner.Action = action;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(validDisplayName);
|
||||||
|
Assert.True(updated);
|
||||||
|
Assert.Equal("Run owner/repo/subdir@v1", _actionRunner.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
|
|||||||
550
src/Test/L0/Worker/PipelineTemplateEvaluatorWrapperL0.cs
Normal file
550
src/Test/L0/Worker/PipelineTemplateEvaluatorWrapperL0.cs
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker;
|
||||||
|
using LegacyContextData = GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using LegacyExpressions = GitHub.DistributedTask.Expressions2;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Tests.Worker
|
||||||
|
{
|
||||||
|
public sealed class PipelineTemplateEvaluatorWrapperL0
|
||||||
|
{
|
||||||
|
private CancellationTokenSource _ecTokenSource;
|
||||||
|
private Mock<IExecutionContext> _ec;
|
||||||
|
private TestHostContext _hc;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// EvaluateAndCompare core behavior
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateAndCompare_DoesNotRecordMismatch_WhenResultsMatch()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
|
||||||
|
var token = new StringToken(null, null, null, "test-value");
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
||||||
|
|
||||||
|
Assert.Equal("test-value", result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateAndCompare_SkipsMismatchRecording_WhenCancellationOccursDuringEvaluation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
|
||||||
|
// Call EvaluateAndCompare directly: the new evaluator cancels the token
|
||||||
|
// and returns a different value, forcing hasMismatch = true.
|
||||||
|
// Because cancellation flipped during the evaluation window, the
|
||||||
|
// mismatch should be skipped.
|
||||||
|
var result = wrapper.EvaluateAndCompare<string, string>(
|
||||||
|
"TestCancellationSkip",
|
||||||
|
() => "legacy-value",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_ecTokenSource.Cancel();
|
||||||
|
return "different-value";
|
||||||
|
},
|
||||||
|
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
Assert.Equal("legacy-value", result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateAndCompare_RecordsMismatch_WhenResultsDifferWithoutCancellation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
|
||||||
|
// Different results without cancellation — mismatch SHOULD be recorded.
|
||||||
|
var result = wrapper.EvaluateAndCompare<string, string>(
|
||||||
|
"TestMismatchRecorded",
|
||||||
|
() => "legacy-value",
|
||||||
|
() => "different-value",
|
||||||
|
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
Assert.Equal("legacy-value", result);
|
||||||
|
Assert.True(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Smoke tests — both parsers agree, no mismatch recorded
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateStepContinueOnError_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new BooleanToken(null, null, null, true);
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateStepContinueOnError(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateStepEnvironment_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new MappingToken(null, null, null);
|
||||||
|
token.Add(new StringToken(null, null, null, "FOO"), new StringToken(null, null, null, "bar"));
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateStepEnvironment(token, contextData, functions, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("bar", result["FOO"]);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateStepIf_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new BasicExpressionToken(null, null, null, "true");
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
var expressionState = new List<KeyValuePair<string, object>>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateStepIf(token, contextData, functions, expressionState);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateStepInputs_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new MappingToken(null, null, null);
|
||||||
|
token.Add(new StringToken(null, null, null, "input1"), new StringToken(null, null, null, "val1"));
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateStepInputs(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("val1", result["input1"]);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateStepTimeout_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new NumberToken(null, null, null, 10);
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateStepTimeout(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.Equal(10, result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateJobContainer_EmptyImage_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new StringToken(null, null, null, "");
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateJobOutput_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new MappingToken(null, null, null);
|
||||||
|
token.Add(new StringToken(null, null, null, "out1"), new StringToken(null, null, null, "val1"));
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateJobOutput(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("val1", result["out1"]);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateEnvironmentUrl_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new StringToken(null, null, null, "https://example.com");
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateEnvironmentUrl(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var stringResult = result as StringToken;
|
||||||
|
Assert.NotNull(stringResult);
|
||||||
|
Assert.Equal("https://example.com", stringResult.Value);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateJobDefaultsRun_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var token = new MappingToken(null, null, null);
|
||||||
|
token.Add(new StringToken(null, null, null, "shell"), new StringToken(null, null, null, "bash"));
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateJobDefaultsRun(token, contextData, functions);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("bash", result["shell"]);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateJobServiceContainers_Null_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateJobServiceContainers(null, contextData, functions);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateJobSnapshotRequest_Null_BothParsersAgree()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
var contextData = new DictionaryContextData();
|
||||||
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
var result = wrapper.EvaluateJobSnapshotRequest(null, contextData, functions);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// JSON parse error equivalence via EvaluateAndCompare
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateAndCompare_JsonReaderExceptions_TreatedAsEquivalent()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
|
||||||
|
// Both throw JsonReaderException with different messages — should be treated as equivalent
|
||||||
|
var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken from JsonReader. Path '', line 0, position 0.");
|
||||||
|
var newEx = new Newtonsoft.Json.JsonReaderException("Error parsing fromJson", new Newtonsoft.Json.JsonReaderException("Unexpected end"));
|
||||||
|
|
||||||
|
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() =>
|
||||||
|
wrapper.EvaluateAndCompare<string, string>(
|
||||||
|
"TestJsonEquivalence",
|
||||||
|
() => throw legacyEx,
|
||||||
|
() => throw newEx,
|
||||||
|
(a, b) => string.Equals(a, b, StringComparison.Ordinal)));
|
||||||
|
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateAndCompare_MixedJsonExceptionTypes_TreatedAsEquivalent()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
|
||||||
|
// Legacy throws Newtonsoft JsonReaderException, new throws System.Text.Json.JsonException
|
||||||
|
var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken");
|
||||||
|
var newEx = new System.Text.Json.JsonException("Error parsing fromJson");
|
||||||
|
|
||||||
|
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() =>
|
||||||
|
wrapper.EvaluateAndCompare<string, string>(
|
||||||
|
"TestMixedJsonTypes",
|
||||||
|
() => throw legacyEx,
|
||||||
|
() => throw newEx,
|
||||||
|
(a, b) => string.Equals(a, b, StringComparison.Ordinal)));
|
||||||
|
|
||||||
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void EvaluateAndCompare_NonJsonExceptions_RecordsMismatch()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||||
|
|
||||||
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||||
|
|
||||||
|
// Both throw non-JSON exceptions with different messages — should record mismatch
|
||||||
|
var legacyEx = new InvalidOperationException("some error");
|
||||||
|
var newEx = new InvalidOperationException("different error");
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
wrapper.EvaluateAndCompare<string, string>(
|
||||||
|
"TestNonJsonMismatch",
|
||||||
|
() => throw legacyEx,
|
||||||
|
() => throw newEx,
|
||||||
|
(a, b) => string.Equals(a, b, StringComparison.Ordinal)));
|
||||||
|
|
||||||
|
Assert.True(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void Setup([CallerMemberName] string name = "")
|
||||||
|
{
|
||||||
|
_ecTokenSource?.Dispose();
|
||||||
|
_ecTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_hc = new TestHostContext(this, name);
|
||||||
|
|
||||||
|
var expressionValues = new LegacyContextData.DictionaryContextData();
|
||||||
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||||
|
|
||||||
|
_ec = new Mock<IExecutionContext>();
|
||||||
|
_ec.Setup(x => x.Global)
|
||||||
|
.Returns(new GlobalContext
|
||||||
|
{
|
||||||
|
FileTable = new List<String>(),
|
||||||
|
Variables = new Variables(_hc, new Dictionary<string, VariableValue>()),
|
||||||
|
WriteDebug = true,
|
||||||
|
});
|
||||||
|
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
|
||||||
|
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
||||||
|
_ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions);
|
||||||
|
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
|
||||||
|
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Teardown()
|
||||||
|
{
|
||||||
|
_hc?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
|
|||||||
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
|
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
|
||||||
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
||||||
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
||||||
DOTNETSDK_VERSION="8.0.416"
|
DOTNETSDK_VERSION="8.0.418"
|
||||||
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
||||||
RUNNER_VERSION=$(cat runnerversion)
|
RUNNER_VERSION=$(cat runnerversion)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "8.0.416"
|
"version": "8.0.418"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user