mirror of
https://github.com/actions/runner.git
synced 2026-02-11 16:03:51 +08:00
Compare commits
1 Commits
dependabot
...
users/eric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90da9fbb8c |
@@ -28,8 +28,8 @@ Debian based OS (Debian, Ubuntu, Linux Mint)
|
||||
- liblttng-ust1 or liblttng-ust0
|
||||
- libkrb5-3
|
||||
- zlib1g
|
||||
- libssl3t64, libssl3, libssl1.1, libssl1.0.2 or libssl1.0.0
|
||||
- libicu76, libicu75, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
|
||||
- libssl1.1, libssl1.0.2 or libssl1.0.0
|
||||
- libicu63, libicu60, libicu57 or libicu55
|
||||
|
||||
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-c
|
||||
&& unzip ./runner-container-hooks.zip -d ./k8s \
|
||||
&& rm runner-container-hooks.zip
|
||||
|
||||
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.1/actions-runner-hooks-k8s-0.8.1.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 \
|
||||
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
|
||||
&& rm runner-container-hooks.zip
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
apt_get_with_fallbacks libssl3t64$ libssl3$ libssl1.1$ libssl1.0.2$ libssl1.0.0$
|
||||
apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'$apt_get' failed with exit code '$?'"
|
||||
|
||||
@@ -172,6 +172,7 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_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 CutoverWorkflowParser = "actions_runner_cutover_workflow_parser";
|
||||
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
|
||||
public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations";
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace GitHub.Runner.Listener
|
||||
public interface IJobDispatcher : IRunnerService
|
||||
{
|
||||
bool Busy { get; }
|
||||
TaskCompletionSource<TaskResult> RunOnceJobCompleted { get; }
|
||||
TaskCompletionSource<bool> RunOnceJobCompleted { get; }
|
||||
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
|
||||
bool Cancel(JobCancelMessage message);
|
||||
Task WaitAsync(CancellationToken token);
|
||||
@@ -56,7 +56,7 @@ namespace GitHub.Runner.Listener
|
||||
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
|
||||
private TimeSpan _channelTimeout;
|
||||
|
||||
private TaskCompletionSource<TaskResult> _runOnceJobCompleted = new();
|
||||
private TaskCompletionSource<bool> _runOnceJobCompleted = new();
|
||||
|
||||
public event EventHandler<JobStatusEventArgs> JobStatus;
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds.");
|
||||
}
|
||||
|
||||
public TaskCompletionSource<TaskResult> RunOnceJobCompleted => _runOnceJobCompleted;
|
||||
public TaskCompletionSource<bool> RunOnceJobCompleted => _runOnceJobCompleted;
|
||||
|
||||
public bool Busy { get; private set; }
|
||||
|
||||
@@ -340,19 +340,18 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
{
|
||||
var jobResult = TaskResult.Succeeded;
|
||||
try
|
||||
{
|
||||
jobResult = await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
|
||||
await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Trace.Info("Fire signal for one time used runner.");
|
||||
_runOnceJobCompleted.TrySetResult(jobResult);
|
||||
_runOnceJobCompleted.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
{
|
||||
Busy = true;
|
||||
try
|
||||
@@ -400,7 +399,7 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
// renew job request task complete means we run out of retry for the first job request renew.
|
||||
Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker.");
|
||||
return TaskResult.Abandoned;
|
||||
return;
|
||||
}
|
||||
|
||||
if (jobRequestCancellationToken.IsCancellationRequested)
|
||||
@@ -413,7 +412,7 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
// complete job request with result Cancelled
|
||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
|
||||
return TaskResult.Canceled;
|
||||
return;
|
||||
}
|
||||
|
||||
HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}");
|
||||
@@ -524,7 +523,7 @@ namespace GitHub.Runner.Listener
|
||||
await renewJobRequest;
|
||||
|
||||
// not finish the job request since the job haven't run on worker at all, we will not going to set a result to server.
|
||||
return TaskResult.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
// we get first jobrequest renew succeed and start the worker process with the job message.
|
||||
@@ -605,7 +604,7 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error(detailInfo);
|
||||
}
|
||||
|
||||
return TaskResultUtil.TranslateFromReturnCode(returnCode);
|
||||
return;
|
||||
}
|
||||
else if (completedTask == renewJobRequest)
|
||||
{
|
||||
@@ -707,8 +706,6 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
// complete job request
|
||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
|
||||
|
||||
return resultOnAbandonOrCancel;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -5,8 +5,8 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -324,11 +324,8 @@ namespace GitHub.Runner.Listener
|
||||
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
|
||||
}
|
||||
|
||||
// hosted runner only run one job and would like to know the result of the job for telemetry and alerting on failure spike.
|
||||
var returnJobResultForHosted = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED"));
|
||||
|
||||
// Run the runner interactively or as service
|
||||
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral || returnJobResultForHosted, returnJobResultForHosted);
|
||||
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -404,32 +401,17 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
//create worker manager, create message listener and start listening to the queue
|
||||
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false, bool returnRunOnceJobResult = false)
|
||||
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
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
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
RunnerSettings migratedSettings = null;
|
||||
|
||||
try
|
||||
|
||||
try
|
||||
{
|
||||
migratedSettings = configManager.LoadMigratedSettings();
|
||||
Trace.Info("Loaded migrated settings from .runner_migrated file");
|
||||
@@ -440,15 +422,15 @@ namespace GitHub.Runner.Listener
|
||||
// If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
|
||||
Trace.Info($"Failed to load migrated settings: {ex.Message}");
|
||||
}
|
||||
|
||||
|
||||
bool usedMigratedSettings = false;
|
||||
|
||||
|
||||
if (migratedSettings != null)
|
||||
{
|
||||
// Try to create session with migrated settings first
|
||||
Trace.Info("Attempting to create session using migrated settings");
|
||||
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||
@@ -468,7 +450,7 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error($"Exception when creating session with migrated settings: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If migrated settings weren't used or session creation failed, use original settings
|
||||
if (!usedMigratedSettings)
|
||||
{
|
||||
@@ -521,7 +503,7 @@ namespace GitHub.Runner.Listener
|
||||
restartSession = true;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
TaskAgentMessage message = null;
|
||||
bool skipMessageDeletion = false;
|
||||
try
|
||||
@@ -583,21 +565,6 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info($"Ignore any exception after cancel message loop. {ex}");
|
||||
}
|
||||
|
||||
if (returnRunOnceJobResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jobResult = await jobDispatcher.RunOnceJobCompleted.Task;
|
||||
return TaskResultUtil.TranslateToReturnCode(jobResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("run once job finished with error.");
|
||||
Trace.Error(ex);
|
||||
return Constants.Runner.ReturnCode.TerminatedError;
|
||||
}
|
||||
}
|
||||
|
||||
return Constants.Runner.ReturnCode.Success;
|
||||
}
|
||||
}
|
||||
@@ -884,15 +851,15 @@ namespace GitHub.Runner.Listener
|
||||
return Constants.Runner.ReturnCode.Success;
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce, bool returnRunOnceJobResult)
|
||||
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce)
|
||||
{
|
||||
int returnCode = Constants.Runner.ReturnCode.Success;
|
||||
bool restart = false;
|
||||
do
|
||||
{
|
||||
restart = false;
|
||||
returnCode = await RunAsync(settings, runOnce, returnRunOnceJobResult);
|
||||
|
||||
returnCode = await RunAsync(settings, runOnce);
|
||||
|
||||
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
|
||||
{
|
||||
Trace.Info("Runner configuration was refreshed, restarting session...");
|
||||
|
||||
@@ -318,17 +318,6 @@ namespace GitHub.Runner.Worker
|
||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
||||
}
|
||||
|
||||
if (!context.Global.HasDeprecatedSetOutput)
|
||||
{
|
||||
context.Global.HasDeprecatedSetOutput = true;
|
||||
var telemetry = new JobTelemetry
|
||||
{
|
||||
Type = JobTelemetryType.ActionCommand,
|
||||
Message = "DeprecatedCommand: set-output"
|
||||
};
|
||||
context.Global.JobTelemetry.Add(telemetry);
|
||||
}
|
||||
|
||||
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
|
||||
{
|
||||
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
|
||||
@@ -364,17 +353,6 @@ namespace GitHub.Runner.Worker
|
||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
||||
}
|
||||
|
||||
if (!context.Global.HasDeprecatedSaveState)
|
||||
{
|
||||
context.Global.HasDeprecatedSaveState = true;
|
||||
var telemetry = new JobTelemetry
|
||||
{
|
||||
Type = JobTelemetryType.ActionCommand,
|
||||
Message = "DeprecatedCommand: save-state"
|
||||
};
|
||||
context.Global.JobTelemetry.Add(telemetry);
|
||||
}
|
||||
|
||||
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
|
||||
{
|
||||
throw new Exception("Required field 'name' is missing in ##[save-state] command.");
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
executionContext,
|
||||
"Load",
|
||||
() => _legacyManager.Load(executionContext, manifestFile),
|
||||
@@ -53,7 +53,7 @@ namespace GitHub.Runner.Worker
|
||||
TemplateToken token,
|
||||
IDictionary<string, PipelineContextData> extraExpressionValues)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
executionContext,
|
||||
"EvaluateCompositeOutputs",
|
||||
() => _legacyManager.EvaluateCompositeOutputs(executionContext, token, extraExpressionValues),
|
||||
@@ -66,7 +66,7 @@ namespace GitHub.Runner.Worker
|
||||
SequenceToken token,
|
||||
IDictionary<string, PipelineContextData> extraExpressionValues)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
executionContext,
|
||||
"EvaluateContainerArguments",
|
||||
() => _legacyManager.EvaluateContainerArguments(executionContext, token, extraExpressionValues),
|
||||
@@ -79,12 +79,13 @@ namespace GitHub.Runner.Worker
|
||||
MappingToken token,
|
||||
IDictionary<string, PipelineContextData> extraExpressionValues)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
executionContext,
|
||||
"EvaluateContainerEnvironment",
|
||||
() => _legacyManager.EvaluateContainerEnvironment(executionContext, token, 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));
|
||||
return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment");
|
||||
});
|
||||
@@ -95,7 +96,7 @@ namespace GitHub.Runner.Worker
|
||||
string inputName,
|
||||
TemplateToken token)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
executionContext,
|
||||
"EvaluateDefaultInput",
|
||||
() => _legacyManager.EvaluateDefaultInput(executionContext, inputName, token),
|
||||
@@ -216,13 +217,27 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Comparison helper methods
|
||||
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
||||
private TLegacy EvaluateWrapper<TLegacy, TNew>(
|
||||
IExecutionContext context,
|
||||
string methodName,
|
||||
Func<TLegacy> legacyEvaluator,
|
||||
Func<TNew> newEvaluator,
|
||||
Func<TLegacy, TNew, bool> resultComparer)
|
||||
{
|
||||
// Cutover: use only the new evaluator, convert result to legacy type
|
||||
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CutoverWorkflowParser) ?? false)
|
||||
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_CUTOVER_WORKFLOW_PARSER")))
|
||||
{
|
||||
var newResult = newEvaluator();
|
||||
if (typeof(TLegacy) == typeof(TNew))
|
||||
{
|
||||
return (TLegacy)(object)newResult;
|
||||
}
|
||||
|
||||
var json = StringUtil.ConvertToJson(newResult, Newtonsoft.Json.Formatting.None);
|
||||
return StringUtil.ConvertFromJson<TLegacy>(json);
|
||||
}
|
||||
|
||||
// Legacy only?
|
||||
if (!((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false)
|
||||
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER"))))
|
||||
|
||||
@@ -1416,7 +1416,8 @@ namespace GitHub.Runner.Worker
|
||||
public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
|
||||
{
|
||||
// Create wrapper?
|
||||
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER")))
|
||||
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER"))
|
||||
|| (context.Global.Variables.GetBoolean(Constants.Runner.Features.CutoverWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_CUTOVER_WORKFLOW_PARSER")))
|
||||
{
|
||||
return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(traceWriter);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,5 @@ namespace GitHub.Runner.Worker
|
||||
public JObject ContainerHookState { get; set; }
|
||||
public bool HasTemplateEvaluatorMismatch { get; set; }
|
||||
public bool HasActionManifestMismatch { get; set; }
|
||||
public bool HasDeprecatedSetOutput { get; set; }
|
||||
public bool HasDeprecatedSaveState { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
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 GitHub.Actions.WorkflowParser;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
@@ -19,6 +19,7 @@ namespace GitHub.Runner.Worker
|
||||
private WorkflowTemplateEvaluator _newEvaluator;
|
||||
private IExecutionContext _context;
|
||||
private Tracing _trace;
|
||||
private bool _cutover;
|
||||
|
||||
public PipelineTemplateEvaluatorWrapper(
|
||||
IHostContext hostContext,
|
||||
@@ -29,6 +30,8 @@ namespace GitHub.Runner.Worker
|
||||
ArgUtil.NotNull(context, nameof(context));
|
||||
_context = context;
|
||||
_trace = hostContext.GetTrace(nameof(PipelineTemplateEvaluatorWrapper));
|
||||
_cutover = (context.Global.Variables.GetBoolean(Constants.Runner.Features.CutoverWorkflowParser) ?? false)
|
||||
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_CUTOVER_WORKFLOW_PARSER"));
|
||||
|
||||
if (traceWriter == null)
|
||||
{
|
||||
@@ -55,7 +58,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateStepContinueOnError",
|
||||
() => _legacyEvaluator.EvaluateStepContinueOnError(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateStepContinueOnError(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -67,7 +70,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateStepDisplayName",
|
||||
() => _legacyEvaluator.EvaluateStepDisplayName(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateStepName(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -80,7 +83,7 @@ namespace GitHub.Runner.Worker
|
||||
IList<IFunctionInfo> expressionFunctions,
|
||||
StringComparer keyComparer)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateStepEnvironment",
|
||||
() => _legacyEvaluator.EvaluateStepEnvironment(token, contextData, expressionFunctions, keyComparer),
|
||||
() => _newEvaluator.EvaluateStepEnvironment(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions), keyComparer),
|
||||
@@ -93,7 +96,7 @@ namespace GitHub.Runner.Worker
|
||||
IList<IFunctionInfo> expressionFunctions,
|
||||
IEnumerable<KeyValuePair<string, object>> expressionState)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateStepIf",
|
||||
() => _legacyEvaluator.EvaluateStepIf(token, contextData, expressionFunctions, expressionState),
|
||||
() => _newEvaluator.EvaluateStepIf(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions), expressionState),
|
||||
@@ -105,7 +108,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateStepInputs",
|
||||
() => _legacyEvaluator.EvaluateStepInputs(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateStepInputs(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -117,7 +120,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateStepTimeout",
|
||||
() => _legacyEvaluator.EvaluateStepTimeout(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateStepTimeout(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -129,7 +132,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateJobContainer",
|
||||
() => _legacyEvaluator.EvaluateJobContainer(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateJobContainer(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -141,7 +144,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateJobOutput",
|
||||
() => _legacyEvaluator.EvaluateJobOutput(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateJobOutputs(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -153,7 +156,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateEnvironmentUrl",
|
||||
() => _legacyEvaluator.EvaluateEnvironmentUrl(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateJobEnvironmentUrl(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -165,7 +168,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateJobDefaultsRun",
|
||||
() => _legacyEvaluator.EvaluateJobDefaultsRun(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateJobDefaultsRun(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -177,7 +180,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateJobServiceContainers",
|
||||
() => _legacyEvaluator.EvaluateJobServiceContainers(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateJobServiceContainers(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -189,7 +192,7 @@ namespace GitHub.Runner.Worker
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
return EvaluateAndCompare(
|
||||
return EvaluateWrapper(
|
||||
"EvaluateJobSnapshotRequest",
|
||||
() => _legacyEvaluator.EvaluateJobSnapshotRequest(token, contextData, expressionFunctions),
|
||||
() => _newEvaluator.EvaluateSnapshot(string.Empty, ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
||||
@@ -216,12 +219,25 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
||||
private TLegacy EvaluateWrapper<TLegacy, TNew>(
|
||||
string methodName,
|
||||
Func<TLegacy> legacyEvaluator,
|
||||
Func<TNew> newEvaluator,
|
||||
Func<TLegacy, TNew, bool> resultComparer)
|
||||
{
|
||||
// Cutover: use only the new evaluator, convert result to legacy type
|
||||
if (_cutover)
|
||||
{
|
||||
var newResult = newEvaluator();
|
||||
if (typeof(TLegacy) == typeof(TNew))
|
||||
{
|
||||
return (TLegacy)(object)newResult;
|
||||
}
|
||||
|
||||
var json = StringUtil.ConvertToJson(newResult, Newtonsoft.Json.Formatting.None);
|
||||
return StringUtil.ConvertFromJson<TLegacy>(json);
|
||||
}
|
||||
|
||||
// Legacy evaluator
|
||||
var legacyException = default(Exception);
|
||||
var legacyResult = default(TLegacy);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<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.Pkcs" Version="10.0.2" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||
<PackageReference Include="Minimatch" Version="2.0.0" />
|
||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
|
||||
@@ -739,8 +739,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent.");
|
||||
if (jobDispatcher.RunOnceJobCompleted.Task.IsCompleted)
|
||||
{
|
||||
var result = await jobDispatcher.RunOnceJobCompleted.Task;
|
||||
Assert.Equal(TaskResult.Succeeded, result);
|
||||
Assert.True(await jobDispatcher.RunOnceJobCompleted.Task, "JobDispatcher should set task complete token to 'TRUE' for one time agent.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,13 +295,13 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
|
||||
var runOnceJobCompleted = new TaskCompletionSource<bool>();
|
||||
_jobDispatcher.Setup(x => x.RunOnceJobCompleted)
|
||||
.Returns(runOnceJobCompleted);
|
||||
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
|
||||
.Callback(() =>
|
||||
{
|
||||
runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
|
||||
runOnceJobCompleted.TrySetResult(true);
|
||||
});
|
||||
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
|
||||
.Callback(() =>
|
||||
@@ -399,13 +399,13 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
|
||||
var runOnceJobCompleted = new TaskCompletionSource<bool>();
|
||||
_jobDispatcher.Setup(x => x.RunOnceJobCompleted)
|
||||
.Returns(runOnceJobCompleted);
|
||||
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
|
||||
.Callback(() =>
|
||||
{
|
||||
runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
|
||||
runOnceJobCompleted.TrySetResult(true);
|
||||
});
|
||||
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
|
||||
.Callback(() =>
|
||||
@@ -733,8 +733,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
|
||||
var completedTask = new TaskCompletionSource<TaskResult>();
|
||||
completedTask.SetResult(TaskResult.Succeeded);
|
||||
var completedTask = new TaskCompletionSource<bool>();
|
||||
completedTask.SetResult(true);
|
||||
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
|
||||
|
||||
//Act
|
||||
@@ -834,8 +834,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
|
||||
var completedTask = new TaskCompletionSource<TaskResult>();
|
||||
completedTask.SetResult(TaskResult.Succeeded);
|
||||
var completedTask = new TaskCompletionSource<bool>();
|
||||
completedTask.SetResult(true);
|
||||
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
|
||||
|
||||
//Act
|
||||
@@ -954,8 +954,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
|
||||
var completedTask = new TaskCompletionSource<TaskResult>();
|
||||
completedTask.SetResult(TaskResult.Succeeded);
|
||||
var completedTask = new TaskCompletionSource<bool>();
|
||||
completedTask.SetResult(true);
|
||||
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
|
||||
|
||||
//Act
|
||||
|
||||
@@ -457,8 +457,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
new SetEnvCommandExtension(),
|
||||
new WarningCommandExtension(),
|
||||
new AddMaskCommandExtension(),
|
||||
new SetOutputCommandExtension(),
|
||||
new SaveStateCommandExtension(),
|
||||
};
|
||||
foreach (var command in commands)
|
||||
{
|
||||
@@ -501,53 +499,5 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
456
src/Test/L0/Worker/ActionManifestParserComparisonL0.cs
Normal file
456
src/Test/L0/Worker/ActionManifestParserComparisonL0.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
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 EvaluateWrapper_SkipsMismatchRecording_WhenCancellationOccursDuringEvaluation()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange - Test that mismatches are not recorded when cancellation state changes during evaluation
|
||||
Setup();
|
||||
|
||||
// Enable comparison feature
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||
|
||||
// Create the wrapper
|
||||
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||
|
||||
// Create a simple token for evaluation
|
||||
var token = new StringToken(null, null, null, "test-value");
|
||||
var contextData = new DictionaryContextData();
|
||||
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||
|
||||
// First evaluation without cancellation - should work normally
|
||||
var result1 = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
||||
Assert.Equal("test-value", result1);
|
||||
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||
|
||||
// Now simulate a scenario where cancellation occurs during evaluation
|
||||
// Cancel the token before next evaluation
|
||||
_ecTokenSource.Cancel();
|
||||
|
||||
// Evaluate again - even if there were a mismatch, it should be skipped due to cancellation
|
||||
var result2 = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
||||
Assert.Equal("test-value", result2);
|
||||
|
||||
// Verify no mismatch was recorded (cancellation race detection should have prevented it)
|
||||
// Note: In this test, both parsers return the same result, so there's no actual mismatch.
|
||||
// The cancellation race detection is a safeguard for when results differ due to timing.
|
||||
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void EvaluateWrapper_DoesNotRecordMismatch_WhenResultsMatch()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange - Test that no mismatch is recorded when both parsers return matching results
|
||||
Setup();
|
||||
|
||||
// Enable comparison feature
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||
|
||||
// Create the wrapper
|
||||
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||
|
||||
// Create a simple token for evaluation
|
||||
var token = new StringToken(null, null, null, "test-value");
|
||||
var contextData = new DictionaryContextData();
|
||||
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||
|
||||
// Evaluation without cancellation - should work normally and not record mismatch for matching results
|
||||
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
||||
Assert.Equal("test-value", result);
|
||||
|
||||
// Since both parsers return the same result, no mismatch should be recorded
|
||||
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void CutoverFlag_UsesNewEvaluator_ForPipelineTemplateEvaluator()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange - Test that cutover flag causes the wrapper to use only the new evaluator
|
||||
Setup();
|
||||
|
||||
// Enable cutover feature (not comparison)
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CutoverWorkflowParser, "true");
|
||||
|
||||
// Create the wrapper
|
||||
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
|
||||
|
||||
// Create a simple token for evaluation
|
||||
var token = new StringToken(null, null, null, "test-value");
|
||||
var contextData = new DictionaryContextData();
|
||||
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
||||
|
||||
// Act - Evaluate in cutover mode
|
||||
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
||||
|
||||
// Assert - Should get the correct result from the new evaluator
|
||||
Assert.Equal("test-value", result);
|
||||
|
||||
// No mismatch should be recorded (comparison is skipped entirely in cutover mode)
|
||||
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void CutoverFlag_UsesNewManager_ForActionManifestLoad()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange - Test that cutover flag causes the manifest wrapper to use only the new manager
|
||||
Setup();
|
||||
|
||||
// Enable cutover feature (not comparison)
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CutoverWorkflowParser, "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 in cutover mode
|
||||
var result = wrapper.Load(_ec.Object, manifestPath);
|
||||
|
||||
// Assert - Should get the correct result from the new manager
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType);
|
||||
|
||||
// No mismatch should be recorded (comparison is skipped in cutover mode)
|
||||
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void CutoverFlag_TakesPrecedence_OverCompareFlag()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange - Test that cutover flag takes precedence over compare flag
|
||||
Setup();
|
||||
|
||||
// Enable both flags
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CutoverWorkflowParser, "true");
|
||||
|
||||
// Create the wrapper
|
||||
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>();
|
||||
|
||||
// Act - Evaluate (cutover should take precedence, skipping comparison entirely)
|
||||
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
||||
|
||||
// Assert - Should get correct result, no comparison mismatch recorded
|
||||
Assert.Equal("test-value", result);
|
||||
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user