Compare commits

...

6 Commits

Author SHA1 Message Date
Salman Muin Kayser Chishti
9433bf68ab Sort deprecated actions list for deterministic annotation output 2026-02-11 13:47:09 +00:00
Salman Muin Kayser Chishti
143730289c Clarify env vars can be set on runner or in workflow file 2026-02-11 13:46:29 +00:00
Salman Muin Kayser Chishti
7caf050db3 Add opt-in/opt-out environment variable guidance to deprecation warning 2026-02-11 13:45:51 +00:00
Salman Muin Kayser Chishti
f19c4ee70e Improve deprecation message: list actions running on node20, suggest checking for updates 2026-02-11 13:43:46 +00:00
Salman Muin Kayser Chishti
af8c4aa59d Fix deprecation message wording: actions will be forced to node24, not stop working 2026-02-11 13:42:29 +00:00
Salman Muin Kayser Chishti
bf5f154d63 Add Node.js 20 deprecation warning annotation
When the actions.runner.warnonnode20 feature flag is enabled, the runner
collects all actions using Node.js 20 (including node12/16 that get
migrated to node20) during the job and emits a single warning annotation
at job finalization:

"Node.js 20 actions are deprecated and will stop working on June 2nd,
2025. Please update the following actions to use Node.js 24: {actions}.
For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/"

Also adds the blog post link to the Phase 2 (node24 default) info message.
2026-02-11 13:39:16 +00:00
6 changed files with 248 additions and 2 deletions

View File

@@ -189,6 +189,10 @@ namespace GitHub.Runner.Common
// Feature flags for controlling the migration phases
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
public static readonly string WarnOnNode20Flag = "actions.runner.warnonnode20";
// Blog post URL for Node 20 deprecation
public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/";
}
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";

View File

@@ -837,6 +837,9 @@ namespace GitHub.Runner.Worker
// Job level annotations
Global.JobAnnotations = new List<Annotation>();
// Track Node.js 20 actions for deprecation warning
Global.DeprecatedNode20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Job Outputs
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);

View File

@@ -31,5 +31,6 @@ namespace GitHub.Runner.Worker
public JObject ContainerHookState { get; set; }
public bool HasTemplateEvaluatorMismatch { get; set; }
public bool HasActionManifestMismatch { get; set; }
public HashSet<string> DeprecatedNode20Actions { get; set; }
}
}

View File

@@ -65,6 +65,20 @@ namespace GitHub.Runner.Worker.Handlers
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
}
// Track Node.js 20 actions for deprecation annotation
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
{
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
if (warnOnNode20)
{
string actionName = GetActionName(action);
if (!string.IsNullOrEmpty(actionName))
{
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
}
}
}
// Check if node20 was explicitly specified in the action
// We don't modify if node24 was explicitly specified
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
@@ -90,7 +104,8 @@ namespace GitHub.Runner.Worker.Handlers
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
{
string infoMessage = "Node 20 is being deprecated. This workflow is running with Node 24 by default. " +
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.";
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. " +
$"For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
executionContext.Output(infoMessage);
}
}
@@ -129,5 +144,18 @@ namespace GitHub.Runner.Worker.Handlers
handler.LocalActionContainerSetupSteps = localActionContainerSetupSteps;
return handler;
}
private static string GetActionName(Pipelines.ActionStepDefinitionReference action)
{
if (action is Pipelines.RepositoryPathReference repoRef)
{
var pathString = string.IsNullOrEmpty(repoRef.Path) ? string.Empty : $"/{repoRef.Path}";
return string.IsNullOrEmpty(repoRef.Ref)
? $"{repoRef.Name}{pathString}"
: $"{repoRef.Name}{pathString}@{repoRef.Ref}";
}
return null;
}
}
}

View File

@@ -735,6 +735,15 @@ namespace GitHub.Runner.Worker
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check service connectivity. {ex.Message}" });
}
}
// Add deprecation warning annotation for Node.js 20 actions
if (context.Global.DeprecatedNode20Actions?.Count > 0)
{
var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
var actionsList = string.Join(", ", sortedActions);
var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2025. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
context.Warning(deprecationMessage);
}
}
catch (Exception ex)
{

View File

@@ -74,7 +74,7 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
@@ -116,5 +116,206 @@ namespace GitHub.Runner.Common.Tests.Worker
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<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 = "actions/checkout",
Ref = "v4"
};
// 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.
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<string, VariableValue>();
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 = "actions/checkout",
Ref = "v4"
};
// 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 - 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<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 = "actions/checkout",
Ref = "v5"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
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 - 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<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/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<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert - node12 gets migrated to node20 and should be tracked
Assert.Contains("some-org/old-action@v1", deprecatedActions);
}
}
}
}