Compare updated workflow parser for ActionManifestManager#4111
Compare updated workflow parser for ActionManifestManager#4111ericsciple merged 1 commit intomainfrom
Conversation
92471cf to
86dcd9a
Compare
| 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 CompareTemplateEvaluator = "actions_runner_compare_template_evaluator"; | ||
| public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser"; |
There was a problem hiding this comment.
Renamed to make it more generic. The action manifest manager also does YAML parsing
There was a problem hiding this comment.
do we ever enable/use the actions_runner_compare_template_evaluator ff, or it's never enable, so the rename is safe.
There was a problem hiding this comment.
Exactly, it doesn't exist yet
| if (File.Exists(manifestFile) || File.Exists(manifestFileYaml)) | ||
| { | ||
| var manifestManager = HostContext.GetService<IActionManifestManager>(); | ||
| var manifestManager = HostContext.GetService<IActionManifestManagerWrapper>(); |
There was a problem hiding this comment.
Now uses a wrapper class, which performs the comparison depending on the feature flag state
| using Pipelines = GitHub.DistributedTask.Pipelines; | ||
| using GitHub.Runner.Common; | ||
| using GitHub.Runner.Sdk; | ||
| using GitHub.Actions.WorkflowParser; |
There was a problem hiding this comment.
Switched to the new namespace
| public interface IActionManifestManager : IRunnerService | ||
| { | ||
| ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile); | ||
| public ActionDefinitionDataNew Load(IExecutionContext executionContext, string manifestFile); |
There was a problem hiding this comment.
Since this struct contains tokens, I had to return a new struct. The wrapper class ActionManifestManagerWrapper handles comparison.
| try | ||
| { | ||
| token = TemplateEvaluator.Evaluate(templateContext, "outputs", token, 0, null, omitHeader: true); | ||
| token = TemplateEvaluator.Evaluate(templateContext, "outputs", token, 0, null); |
There was a problem hiding this comment.
omitHeader: true isn't needed an option and isn't needed in new evaluator.
The new evaluator doesn't add a header trace line.
| public ActionDefinitionDataNew Load(IExecutionContext executionContext, string manifestFile); | ||
|
|
||
| DictionaryContextData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary<string, PipelineContextData> extraExpressionValues); | ||
| DictionaryExpressionData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary<string, ExpressionData> extraExpressionValues); |
There was a problem hiding this comment.
Most of the changes throughout this file is just switching to the new type names
| // Convert old PipelineContextData to new ExpressionData | ||
| var json = StringUtil.ConvertToJson(pair.Value, Newtonsoft.Json.Formatting.None); | ||
| var newValue = StringUtil.ConvertFromJson<GitHub.Actions.Expressions.Data.ExpressionData>(json); | ||
| result.ExpressionValues[pair.Key] = newValue; |
There was a problem hiding this comment.
executionContext.ExpressionValues is the old type. So we need to convert to the new types
| result.ExpressionFunctions.Add(item); | ||
| GitHub.Actions.Expressions.IFunctionInfo newFunc = func.Name switch | ||
| { | ||
| "always" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewAlwaysFunction>(func.Name, func.MinParameters, func.MaxParameters), |
There was a problem hiding this comment.
These new functions were defined with the last PR:
| } | ||
| } | ||
|
|
||
| public sealed class ActionDefinitionDataNew |
There was a problem hiding this comment.
These are temporary. The wrapper class ActionManifestManagerWrapper performs the conversion.
| @@ -0,0 +1,546 @@ | |||
| using System; | |||
| namespace GitHub.Runner.Worker | ||
| { | ||
| [ServiceLocator(Default = typeof(ActionManifestManagerLegacy))] | ||
| public interface IActionManifestManagerLegacy : IRunnerService |
There was a problem hiding this comment.
Renamed to Legacy
| { | ||
| // Create wrapper? | ||
| if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareTemplateEvaluator) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_TEMPLATE_EVALUATOR"))) | ||
| if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER"))) |
There was a problem hiding this comment.
Again, I renamed this flag to be more generic since the action manifest manager also performs YAML parsing, not just template evaluation
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <InternalsVisibleTo Include="Test" /> |
There was a problem hiding this comment.
I am trying to remember why I needed this. I think it was for a template token extension method to assert the type
| Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); | ||
|
|
||
| var containerAction = result.Execution as ContainerActionExecutionData; | ||
| var containerAction = result.Execution as ContainerActionExecutionDataNew; |
| TemplateToken token, | ||
| IDictionary<string, PipelineContextData> extraExpressionValues) | ||
| { | ||
| return EvaluateAndCompare( |
There was a problem hiding this comment.
Helper method checks the feature flag and conditionally calls the new implementation
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a comparison mechanism for an updated workflow parser in ActionManifestManager. The implementation creates a wrapper pattern to compare outputs between the legacy implementation and a new workflow parser-based implementation, enabling safe rollout via a feature flag.
Key changes:
- Extracted existing ActionManifestManager logic into ActionManifestManagerLegacy
- Updated ActionManifestManager to use new GitHub.Actions.WorkflowParser libraries
- Created ActionManifestManagerWrapper to compare both implementations and record telemetry on mismatches
Reviewed Changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 37 comments.
Show a summary per file
| File | Description |
|---|---|
| ActionManifestManagerWrapper.cs | New wrapper class that invokes both legacy and new implementations, compares results, and logs mismatches |
| ActionManifestManagerLegacy.cs | Legacy implementation extracted from ActionManifestManager |
| ActionManifestManager.cs | Updated to use new workflow parser libraries (GitHub.Actions.WorkflowParser, GitHub.Actions.Expressions) |
| ActionManifestManagerLegacyL0.cs | Test file duplicated from ActionManifestManagerL0 for legacy implementation |
| ActionManifestManagerL0.cs | Updated tests to use new types and expression data classes |
| ActionRunnerL0.cs, ActionManagerL0.cs | Test setup updated to initialize all three manifest manager implementations |
| ContainerActionHandler.cs, CompositeActionHandler.cs, ActionRunner.cs, ActionManager.cs | Updated to use IActionManifestManagerWrapper instead of IActionManifestManager |
| GlobalContext.cs | Added HasActionManifestMismatch flag for telemetry |
| ExecutionContext.cs, Constants.cs | Renamed feature flag from CompareTemplateEvaluator to CompareWorkflowParser |
| Sdk.csproj | Added InternalsVisibleTo for Test assembly |
| return messages; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The ActionManifestManagerWrapper class lacks test coverage. This wrapper contains critical comparison logic that should be tested to ensure correct behavior of the mismatch detection, result comparison methods, and conversion helpers. Consider adding ActionManifestManagerWrapperL0.cs with tests covering the comparison scenarios.
| maxBytes: 10 * 1024 * 1024), | ||
| Schema = _actionManifestSchema, | ||
| TraceWriter = executionContext.ToTemplateTraceWriter(), | ||
| // TODO: Switch to real tracewriter for cutover |
There was a problem hiding this comment.
This TODO comment suggests incomplete implementation. The EmptyTraceWriter may cause debugging difficulties during the comparison phase. Consider either implementing the real trace writer before merging or creating a tracking issue and referencing it in the comment (e.g., 'TODO (#issue-number): Switch to real tracewriter for cutover').
| // TODO: Switch to real tracewriter for cutover | |
| // TODO (#12345): Switch to real tracewriter for cutover |
| GitHub.Actions.Expressions.IFunctionInfo newFunc = func.Name switch | ||
| { | ||
| "always" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewAlwaysFunction>(func.Name, func.MinParameters, func.MaxParameters), | ||
| "cancelled" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewCancelledFunction>(func.Name, func.MinParameters, func.MaxParameters), | ||
| "failure" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewFailureFunction>(func.Name, func.MinParameters, func.MaxParameters), | ||
| "success" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewSuccessFunction>(func.Name, func.MinParameters, func.MaxParameters), | ||
| "hashFiles" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewHashFilesFunction>(func.Name, func.MinParameters, func.MaxParameters), | ||
| _ => throw new NotSupportedException($"Expression function '{func.Name}' is not supported in ActionManifestManager") | ||
| }; |
There was a problem hiding this comment.
This function conversion logic is duplicated in PipelineTemplateEvaluatorWrapper.cs (lines 326-334). Consider extracting this into a shared helper method to reduce code duplication and ensure consistency between the two conversion points.
| foreach (var innerEx in aggregateEx.InnerExceptions) | ||
| { | ||
| if (innerEx != null && count < 50) | ||
| { | ||
| toProcess.Enqueue(innerEx); | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| if (legacyDict is Dictionary<string, string> legacyTypedDict && newDict is Dictionary<string, string> newTypedDict) | ||
| { | ||
| if (!object.Equals(legacyTypedDict.Comparer, newTypedDict.Comparer)) | ||
| { | ||
| trace.Info($"CompareDictionaries mismatch - {fieldName} - different comparers (legacy={legacyTypedDict.Comparer.GetType().Name}, new={newTypedDict.Comparer.GetType().Name})"); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
These 'if' statements can be combined.
| if (legacyDict is Dictionary<string, string> legacyTypedDict && newDict is Dictionary<string, string> newTypedDict) | |
| { | |
| if (!object.Equals(legacyTypedDict.Comparer, newTypedDict.Comparer)) | |
| { | |
| trace.Info($"CompareDictionaries mismatch - {fieldName} - different comparers (legacy={legacyTypedDict.Comparer.GetType().Name}, new={newTypedDict.Comparer.GetType().Name})"); | |
| return false; | |
| } | |
| if (legacyDict is Dictionary<string, string> legacyTypedDict && newDict is Dictionary<string, string> newTypedDict | |
| && !object.Equals(legacyTypedDict.Comparer, newTypedDict.Comparer)) | |
| { | |
| trace.Info($"CompareDictionaries mismatch - {fieldName} - different comparers (legacy={legacyTypedDict.Comparer.GetType().Name}, new={newTypedDict.Comparer.GetType().Name})"); | |
| return false; |
| } | ||
| } | ||
| catch (Exception ex) | ||
| { |
There was a problem hiding this comment.
Generic catch clause.
| { | |
| { | |
| // Rethrow critical exceptions | |
| if (ex is OutOfMemoryException || ex is StackOverflowException || ex is ThreadAbortException) | |
| { | |
| throw; | |
| } |
| catch (Exception ex) | ||
| { | ||
| legacyException = ex; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| trace.Info($"Comparison failed: {ex.Message}"); | ||
| RecordComparisonError(context, $"{methodName}: {ex.Message}"); | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| newException = ex; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
86dcd9a to
d5be076
Compare
Follow-up to: