Skip to content

Conversation

@ericsciple
Copy link
Collaborator

@ericsciple ericsciple commented Nov 15, 2025

@ericsciple ericsciple force-pushed the users/ericsciple/25-11-manifest branch from 92471cf to 86dcd9a Compare November 15, 2025 07:45
@ericsciple ericsciple changed the title Users/ericsciple/25 11 manifest Compare updated workflow parser for ActionManifestManager Nov 15, 2025
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";
Copy link
Collaborator Author

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

Copy link
Member

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.

Copy link
Collaborator Author

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>();
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

@ericsciple ericsciple Nov 17, 2025

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);
Copy link
Collaborator Author

@ericsciple ericsciple Nov 17, 2025

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);
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

@ericsciple ericsciple Nov 17, 2025

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),
Copy link
Collaborator Author

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
Copy link
Collaborator Author

@ericsciple ericsciple Nov 17, 2025

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;
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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")))
Copy link
Collaborator Author

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" />
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests pass!

@ericsciple ericsciple marked this pull request as ready for review November 17, 2025 23:13
@ericsciple ericsciple requested a review from a team as a code owner November 17, 2025 23:13
Copilot AI review requested due to automatic review settings November 17, 2025 23:13
TemplateToken token,
IDictionary<string, PipelineContextData> extraExpressionValues)
{
return EvaluateAndCompare(
Copy link
Collaborator Author

@ericsciple ericsciple Nov 17, 2025

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

Copy link
Contributor

Copilot AI left a 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;
}
}
}
Copy link

Copilot AI Nov 17, 2025

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.

Copilot uses AI. Check for mistakes.
maxBytes: 10 * 1024 * 1024),
Schema = _actionManifestSchema,
TraceWriter = executionContext.ToTemplateTraceWriter(),
// TODO: Switch to real tracewriter for cutover
Copy link

Copilot AI Nov 17, 2025

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').

Suggested change
// TODO: Switch to real tracewriter for cutover
// TODO (#12345): Switch to real tracewriter for cutover

Copilot uses AI. Check for mistakes.
Comment on lines +342 to +350
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")
};
Copy link

Copilot AI Nov 17, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +677 to +683
foreach (var innerEx in aggregateEx.InnerExceptions)
{
if (innerEx != null && count < 50)
{
toProcess.Enqueue(innerEx);
}
}
Copy link

Copilot AI Nov 17, 2025

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(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +590 to +596
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;
}
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
}
}
catch (Exception ex)
{
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Suggested change
{
{
// Rethrow critical exceptions
if (ex is OutOfMemoryException || ex is StackOverflowException || ex is ThreadAbortException)
{
throw;
}

Copilot uses AI. Check for mistakes.
Comment on lines +242 to +245
catch (Exception ex)
{
legacyException = ex;
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +290
catch (Exception ex)
{
trace.Info($"Comparison failed: {ex.Message}");
RecordComparisonError(context, $"{methodName}: {ex.Message}");
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +263
catch (Exception ex)
{
newException = ex;
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
@ericsciple ericsciple enabled auto-merge (squash) November 18, 2025 01:11
@ericsciple ericsciple force-pushed the users/ericsciple/25-11-manifest branch from 86dcd9a to d5be076 Compare November 18, 2025 01:11
@ericsciple ericsciple merged commit a54f380 into main Nov 18, 2025
10 checks passed
@ericsciple ericsciple deleted the users/ericsciple/25-11-manifest branch November 18, 2025 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants