Add tests for deprecation warning formatting, truncation, sorting, and sanitization

- HandlerFactoryL0: test subpath tracking and CR/LF sanitization in action names
- JobExtensionL0: test warning emission, no-op when empty, truncation at 10 actions, alphabetical sorting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Salman Chishti
2026-02-18 07:04:24 -08:00
committed by GitHub
parent 0211e73817
commit 061be5238a
2 changed files with 212 additions and 0 deletions

View File

@@ -317,5 +317,109 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Contains("some-org/old-action@v1", deprecatedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_WithSubpath_TrackedCorrectly()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "some-org/monorepo",
Path = "actions/my-action",
Ref = "v2"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert - subpath should be included in tracked name
Assert.Contains("some-org/monorepo/actions/my-action@v2", deprecatedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_SanitizesNewlinesInActionName()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "evil-org/bad\r\naction",
Ref = "v1"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert - CR/LF should be stripped to prevent log/annotation injection
Assert.Contains("evil-org/badaction@v1", deprecatedActions);
Assert.DoesNotContain("evil-org/bad\r\naction@v1", deprecatedActions);
}
}
}
}

View File

@@ -755,5 +755,113 @@ namespace GitHub.Runner.Common.Tests.Worker
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task FinalizeJob_EmitsDeprecationWarning_WhenNode20ActionsExist()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_jobEc = new Runner.Worker.ExecutionContext { Result = TaskResult.Succeeded };
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// Add deprecated actions
_jobEc.Global.DeprecatedNode20Actions.Add("actions/checkout@v4");
_jobEc.Global.DeprecatedNode20Actions.Add("actions/setup-node@v3");
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
// Verify warning was written to log containing the action names
_logger.Verify(x => x.Write(It.Is<string>(s =>
s.Contains("actions/checkout@v4") &&
s.Contains("actions/setup-node@v3") &&
s.Contains("Node.js 20 is approaching end-of-life"))), Times.AtLeastOnce);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task FinalizeJob_NoDeprecationWarning_WhenNoNode20Actions()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_jobEc = new Runner.Worker.ExecutionContext { Result = TaskResult.Succeeded };
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// DeprecatedNode20Actions is initialized but empty
Assert.Empty(_jobEc.Global.DeprecatedNode20Actions);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
// Verify no deprecation warning was emitted
_logger.Verify(x => x.Write(It.Is<string>(s =>
s.Contains("Node.js 20 is approaching end-of-life"))), Times.Never);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task FinalizeJob_TruncatesActionsListAtTen()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_jobEc = new Runner.Worker.ExecutionContext { Result = TaskResult.Succeeded };
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// Add 15 deprecated actions
for (int i = 1; i <= 15; i++)
{
_jobEc.Global.DeprecatedNode20Actions.Add($"org/action-{i:D2}@v1");
}
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
// Verify truncation message appears with correct count
_logger.Verify(x => x.Write(It.Is<string>(s =>
s.Contains("... and 5 more"))), Times.AtLeastOnce);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task FinalizeJob_SortsActionsAlphabetically()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_jobEc = new Runner.Worker.ExecutionContext { Result = TaskResult.Succeeded };
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// Add actions in reverse alphabetical order
_jobEc.Global.DeprecatedNode20Actions.Add("z-org/last-action@v1");
_jobEc.Global.DeprecatedNode20Actions.Add("a-org/first-action@v1");
_jobEc.Global.DeprecatedNode20Actions.Add("m-org/middle-action@v1");
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
// Verify actions appear sorted: a-org before m-org before z-org
_logger.Verify(x => x.Write(It.Is<string>(s =>
s.Contains("a-org/first-action@v1, m-org/middle-action@v1, z-org/last-action@v1"))), Times.AtLeastOnce);
}
}
}
}