using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using Moq; using Xunit; using GitHub.Runner.Worker; using GitHub.Runner.Worker.Handlers; using GitHub.DistributedTask.Pipelines; using GitHub.DistributedTask.WebApi; namespace GitHub.Runner.Common.Tests.Worker { public sealed class HandlerFactoryL0 { private Mock _ec; private TestHostContext CreateTestContext([CallerMemberName] string testName = "") { var hostContext = new TestHostContext(this, testName); _ec = new Mock(); _ec.SetupAllProperties(); _ec.Object.Initialize(hostContext); var handler = new Mock(); handler.SetupAllProperties(); hostContext.EnqueueInstance(handler.Object); //hostContext.EnqueueInstance(new ActionCommandManager() as IActionCommandManager); return hostContext; } [Theory] [Trait("Level", "L0")] [Trait("Category", "Worker")] [InlineData("node12", "node20")] [InlineData("node16", "node20")] [InlineData("node20", "node20")] [InlineData("node24", "node24")] public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion) { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); // Setup variables var variables = new Dictionary(); Variables serverVariables = new(hc, variables); // Workflow opt-out var workflowVariables = new Dictionary(); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = workflowVariables }); // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = inputVersion; var handler = hf.Create( _ec.Object, new ScriptReference(), new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; // Assert. Assert.Equal(expectedVersion, handler.Data.NodeVersion); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node24ExplicitlyRequested_HonoredByDefault() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); // Basic variables setup var variables = new Dictionary(); Variables serverVariables = new(hc, variables); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary() }); // Act - Node 24 explicitly requested in action.yml var data = new NodeJSActionExecutionData(); data.NodeVersion = "node24"; var handler = hf.Create( _ec.Object, new ScriptReference(), new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; // Assert - should be node24 as requested Assert.Equal("node24", handler.Data.NodeVersion); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node20Action_TrackedWhenWarnFlagEnabled() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v4" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ); // Assert. Assert.Contains("actions/checkout@v4", deprecatedActions); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node20Action_NotTrackedWhenWarnFlagDisabled() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary(); Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v4" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ); // Assert - should not track when flag is disabled Assert.Empty(deprecatedActions); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node24Action_NotTrackedEvenWhenWarnFlagEnabled() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v5" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node24"; hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ); // Assert - node24 actions should not be tracked Assert.Empty(deprecatedActions); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node12Action_TrackedAsDeprecatedWhenWarnFlagEnabled() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions }); var actionRef = new RepositoryPathReference { Name = "some-org/old-action", Ref = "v1" }; // Act - node12 gets migrated to node20, then should be tracked var data = new NodeJSActionExecutionData(); data.NodeVersion = "node12"; hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ); // Assert - node12 gets migrated to node20 and should be tracked Assert.Contains("some-org/old-action@v1", deprecatedActions); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void LocalNode20Action_TrackedWhenWarnFlagEnabled() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions }); // Local action: Name is empty, Path is the local path var actionRef = new RepositoryPathReference { Name = "", Path = "./.github/actions/my-action", RepositoryType = "self" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ); // Assert - local action should be tracked with its path Assert.Contains("./.github/actions/my-action", deprecatedActions); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node20Action_TrackedAsUpgradedWhenUseNode24ByDefaultEnabled() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }, { Constants.Runner.NodeMigration.UseNode24ByDefaultFlag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); var upgradedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions, UpgradedToNode24Actions = upgradedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v4" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; // On non-ARM32 platforms, action should be upgraded to node24 // and tracked in UpgradedToNode24Actions, NOT in DeprecatedNode20Actions bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); if (!isArm32Linux) { Assert.Equal("node24", handler.Data.NodeVersion); Assert.Contains("actions/checkout@v4", upgradedActions); Assert.DoesNotContain("actions/checkout@v4", deprecatedActions); } } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node20Action_NotUpgradedWhenPhase1Only() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); var upgradedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), DeprecatedNode20Actions = deprecatedActions, UpgradedToNode24Actions = upgradedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v4" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; // In Phase 1 (no UseNode24ByDefault), action stays on node20 // and should be in DeprecatedNode20Actions Assert.Equal("node20", handler.Data.NodeVersion); Assert.Contains("actions/checkout@v4", deprecatedActions); Assert.Empty(upgradedActions); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void ExplicitNode24Action_KillArm32Flag_ThrowsOnArm32() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.KillLinuxArm32Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary() }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v5" }; // Act - action explicitly declares node24 var data = new NodeJSActionExecutionData(); data.NodeVersion = "node24"; bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); if (isArm32Linux) { // On ARM32 Linux, kill flag should cause the handler to throw Assert.Throws(() => hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() )); } else { // On other platforms, should proceed normally var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; Assert.Equal("node24", handler.Data.NodeVersion); } } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void ExplicitNode24Action_DeprecateArm32Flag_DowngradesToNode20OnArm32() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var arm32Actions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), Arm32Node20Actions = arm32Actions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v5" }; // Act - action explicitly declares node24 var data = new NodeJSActionExecutionData(); data.NodeVersion = "node24"; var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); if (isArm32Linux) { // On ARM32 Linux, should downgrade to node20 and track Assert.Equal("node20", handler.Data.NodeVersion); Assert.Contains("actions/checkout@v5", arm32Actions); } else { // On other platforms, should remain node24 Assert.Equal("node24", handler.Data.NodeVersion); Assert.Empty(arm32Actions); } } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void ExplicitNode24Action_NoArm32Flags_StaysNode24() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary(); Variables serverVariables = new(hc, variables); var arm32Actions = new HashSet(StringComparer.OrdinalIgnoreCase); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), Arm32Node20Actions = arm32Actions, DeprecatedNode20Actions = deprecatedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v5" }; // Act - action explicitly declares node24, no ARM32 flags var data = new NodeJSActionExecutionData(); data.NodeVersion = "node24"; var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; // On non-ARM32 platforms, should stay node24 and not be tracked in any list bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); if (!isArm32Linux) { Assert.Equal("node24", handler.Data.NodeVersion); Assert.Empty(arm32Actions); Assert.Empty(deprecatedActions); } } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node20Action_RequireNode24_ForcesNode24() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.RequireNode24Flag, new VariableValue("true") }, { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); var upgradedActions = new HashSet(StringComparer.OrdinalIgnoreCase); var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary(), UpgradedToNode24Actions = upgradedActions, DeprecatedNode20Actions = deprecatedActions }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v4" }; // Act. var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); if (!isArm32Linux) { var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; // Phase 3: RequireNode24 forces node24, ignoring env vars Assert.Equal("node24", handler.Data.NodeVersion); Assert.Contains("actions/checkout@v4", upgradedActions); Assert.Empty(deprecatedActions); } } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Node20Action_KillArm32Flag_ThrowsOnArm32() { using (TestHostContext hc = CreateTestContext()) { // Arrange. var hf = new HandlerFactory(); hf.Initialize(hc); var variables = new Dictionary { { Constants.Runner.NodeMigration.KillLinuxArm32Flag, new VariableValue("true") } }; Variables serverVariables = new(hc, variables); _ec.Setup(x => x.Global).Returns(new GlobalContext() { Variables = serverVariables, EnvironmentVariables = new Dictionary() }); var actionRef = new RepositoryPathReference { Name = "actions/checkout", Ref = "v4" }; var data = new NodeJSActionExecutionData(); data.NodeVersion = "node20"; bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); if (isArm32Linux) { Assert.Throws(() => hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() )); } else { // On non-ARM32, should proceed normally (node20 stays) var handler = hf.Create( _ec.Object, actionRef, new Mock().Object, data, new Dictionary(), new Dictionary(), new Variables(hc, new Dictionary()), "", new List() ) as INodeScriptActionHandler; Assert.Equal("node20", handler.Data.NodeVersion); } } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void ExplicitNode24Action_DeprecateArm32_UsesOriginalVersionForTracking() { // Regression test: verifies that when an action explicitly declares node24 // and ARM32 deprecation downgrades it to node20, the tracking call uses // the original preferred version ("node24"), not the already-overwritten // nodeData.NodeVersion ("node20"). Without this fix, ShouldTrackAsArm32Node20 // would receive (preferred="node20", final="node20") and never return true. string originalPreferred = "node24"; string finalAfterArm32Downgrade = "node20"; string deprecationWarning = "Linux ARM32 runners are deprecated and will no longer be supported after September 16th, 2026. Please migrate to a supported platform."; // Correct: use the original preferred version before assignment bool correctTracking = HandlerFactory.ShouldTrackAsArm32Node20( deprecateArm32: true, preferredNodeVersion: originalPreferred, finalNodeVersion: finalAfterArm32Downgrade, platformWarningMessage: deprecationWarning); Assert.True(correctTracking); // Bug scenario: if nodeData.NodeVersion was already overwritten to finalNodeVersion bool buggyTracking = HandlerFactory.ShouldTrackAsArm32Node20( deprecateArm32: true, preferredNodeVersion: finalAfterArm32Downgrade, finalNodeVersion: finalAfterArm32Downgrade, platformWarningMessage: deprecationWarning); Assert.False(buggyTracking); } [Theory] [Trait("Level", "L0")] [Trait("Category", "Worker")] [InlineData(true, "node24", "node20", "Linux ARM32 runners are deprecated", true)] [InlineData(true, "node20", "node20", "Linux ARM32 runners are deprecated", false)] [InlineData(true, "node24", "node24", "Linux ARM32 runners are deprecated", false)] [InlineData(true, "node24", "node20", null, false)] [InlineData(false, "node24", "node20", "Linux ARM32 runners are deprecated", false)] public void ShouldTrackAsArm32Node20_ClassifiesOnlyPlatformDowngrades( bool deprecateArm32, string preferredNodeVersion, string finalNodeVersion, string platformWarningMessage, bool expected) { bool actual = HandlerFactory.ShouldTrackAsArm32Node20( deprecateArm32, preferredNodeVersion, finalNodeVersion, platformWarningMessage); Assert.Equal(expected, actual); } } }