-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Compare updated workflow parser for ActionManifestManager #4111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to make it more generic. The action manifest manager also does YAML parsing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These new functions were defined with the last PR:
| } | ||
| } | ||
|
|
||
| public sealed class ActionDefinitionDataNew |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are temporary. The wrapper class ActionManifestManagerWrapper performs the conversion.
| @@ -0,0 +1,546 @@ | |||
| using System; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existing code
| namespace GitHub.Runner.Worker | ||
| { | ||
| [ServiceLocator(Default = typeof(ActionManifestManagerLegacy))] | ||
| public interface IActionManifestManagerLegacy : IRunnerService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests pass!
| TemplateToken token, | ||
| IDictionary<string, PipelineContextData> extraExpressionValues) | ||
| { | ||
| return EvaluateAndCompare( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Helper method checks the feature flag and conditionally calls the new implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; | ||
| } | ||
| } | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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") | ||
| }; |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); | ||
| } | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) | ||
| { |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic catch clause.
| { | |
| { | |
| // Rethrow critical exceptions | |
| if (ex is OutOfMemoryException || ex is StackOverflowException || ex is ThreadAbortException) | |
| { | |
| throw; | |
| } |
| catch (Exception ex) | ||
| { | ||
| legacyException = ex; | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| trace.Info($"Comparison failed: {ex.Message}"); | ||
| RecordComparisonError(context, $"{methodName}: {ex.Message}"); | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| newException = ex; | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic catch clause.
86dcd9a to
d5be076
Compare
Follow-up to: