Skip to content

Commit ed48ddd

Browse files
authored
Runner Support for executing Node24 Actions (#3940)
1 parent a1e6ad8 commit ed48ddd

File tree

13 files changed

+394
-16
lines changed

13 files changed

+394
-16
lines changed

docs/checks/nodejs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
66

7-
The runner carries its own copy of node.js executable under `<runner_root>/externals/node20/`.
7+
The runner carries its own copies of node.js executables under `<runner_root>/externals/node20/` and `<runner_root>/externals/node24/`.
88

9-
All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node20/`.
9+
All javascript base Actions will get executed by the built-in `node` at either `<runner_root>/externals/node20/` or `<runner_root>/externals/node24/` depending on the version specified in the action's metadata.
1010

1111
> Not the `node` from `$PATH`
1212

src/Misc/externals.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
77
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
88
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
99
NODE20_VERSION="20.19.3"
10+
NODE24_VERSION="24.4.0"
1011

1112
get_abs_path() {
1213
# exploits the fact that pwd will print abs path when no args
@@ -139,6 +140,8 @@ function acquireExternalTool() {
139140
if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
140141
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
141142
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
143+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
144+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
142145
if [[ "$PRECACHE" != "" ]]; then
143146
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
144147
fi
@@ -149,6 +152,8 @@ if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
149152
# todo: replace these with official release when available
150153
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
151154
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
155+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
156+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
152157
if [[ "$PRECACHE" != "" ]]; then
153158
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
154159
fi
@@ -157,21 +162,26 @@ fi
157162
# Download the external tools only for OSX.
158163
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
159164
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir
165+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-x64.tar.gz" node24 fix_nested_dir
160166
fi
161167

162168
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
163169
# node.js v12 doesn't support macOS on arm64.
164170
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-arm64.tar.gz" node20 fix_nested_dir
171+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-arm64.tar.gz" node24 fix_nested_dir
165172
fi
166173

167174
# Download the external tools for Linux PACKAGERUNTIMEs.
168175
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
169176
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-x64.tar.gz" node20 fix_nested_dir
170177
acquireExternalTool "$NODE_ALPINE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine
178+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-x64.tar.gz" node24 fix_nested_dir
179+
acquireExternalTool "$NODE_ALPINE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-alpine-x64.tar.gz" node24_alpine
171180
fi
172181

173182
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
174183
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-arm64.tar.gz" node20 fix_nested_dir
184+
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-arm64.tar.gz" node24 fix_nested_dir
175185
fi
176186

177187
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then

src/Misc/layoutbin/update.sh.template

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,17 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
135135
then
136136
# inspect the open file handles to find the node process
137137
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
138-
nodever="node20"
138+
# Try finding node24 first, then fallback to earlier versions if needed
139+
nodever="node24"
139140
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
140-
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
141+
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node20
141142
then
142-
nodever="node16"
143+
nodever="node20"
143144
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
145+
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
146+
then
147+
nodever="node16"
148+
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
144149
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
145150
then
146151
nodever="node12"

src/Runner.Common/Util/NodeUtil.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,22 @@ public static string GetInternalNodeVersion()
1818
}
1919
return _defaultNodeVersion;
2020
}
21+
22+
/// <summary>
23+
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
24+
/// </summary>
25+
/// <param name="preferredVersion">The preferred Node version</param>
26+
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
27+
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
28+
{
29+
if (string.Equals(preferredVersion, "node24", StringComparison.OrdinalIgnoreCase) &&
30+
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
31+
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
32+
{
33+
return ("node20", "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
34+
}
35+
36+
return (preferredVersion, null);
37+
}
2138
}
2239
}

src/Runner.Worker/ActionManifestManager.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,8 @@ private ActionExecutionData ConvertRuns(
450450
}
451451
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
452452
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
453-
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase))
453+
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
454+
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
454455
{
455456
if (string.IsNullOrEmpty(mainToken?.Value))
456457
{
@@ -490,7 +491,7 @@ private ActionExecutionData ConvertRuns(
490491
}
491492
else
492493
{
493-
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16' or 'node20' instead.");
494+
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
494495
}
495496
}
496497
else if (pluginToken != null)
@@ -501,7 +502,7 @@ private ActionExecutionData ConvertRuns(
501502
};
502503
}
503504

504-
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.");
505+
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
505506
}
506507

507508
private void ConvertInputs(

src/Runner.Worker/Handlers/StepHost.cs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using GitHub.DistributedTask.Pipelines.ContextData;
43
using System.Text;
54
using System.Threading;
65
using System.Threading.Tasks;
@@ -9,7 +8,6 @@
98
using GitHub.Runner.Sdk;
109
using System.Linq;
1110
using GitHub.Runner.Worker.Container.ContainerHooks;
12-
using System.IO;
1311
using System.Threading.Channels;
1412

1513
namespace GitHub.Runner.Worker.Handlers
@@ -60,7 +58,14 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string
6058

6159
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
6260
{
63-
return Task.FromResult<string>(preferredVersion);
61+
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
62+
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
63+
if (!string.IsNullOrEmpty(warningMessage))
64+
{
65+
executionContext.Warning(warningMessage);
66+
}
67+
68+
return Task.FromResult(nodeVersion);
6469
}
6570

6671
public async Task<int> ExecuteAsync(IExecutionContext context,
@@ -137,8 +142,12 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string
137142

138143
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
139144
{
140-
// Optimistically use the default
141-
string nodeExternal = preferredVersion;
145+
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
146+
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
147+
if (!string.IsNullOrEmpty(warningMessage))
148+
{
149+
executionContext.Warning(warningMessage);
150+
}
142151

143152
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
144153
{
@@ -264,7 +273,14 @@ await containerHookManager.RunScriptStepAsync(context,
264273

265274
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
266275
{
267-
string nodeExternal = preferredVersion;
276+
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
277+
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
278+
if (!string.IsNullOrEmpty(warningMessage))
279+
{
280+
executionContext.Warning(warningMessage);
281+
}
282+
283+
// Check for Alpine container compatibility
268284
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
269285
{
270286
var os = Constants.Runner.Platform.ToString();

src/Test/L0/Worker/ActionManagerL0.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,76 @@ public void LoadsNode20ActionDefinition()
16591659
Teardown();
16601660
}
16611661
}
1662+
1663+
[Fact]
1664+
[Trait("Level", "L0")]
1665+
[Trait("Category", "Worker")]
1666+
public void LoadsNode24ActionDefinition()
1667+
{
1668+
try
1669+
{
1670+
// Arrange.
1671+
Setup();
1672+
const string Content = @"
1673+
# Container action
1674+
name: 'Hello World'
1675+
description: 'Greet the world and record the time'
1676+
author: 'GitHub'
1677+
inputs:
1678+
greeting: # id of input
1679+
description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout'
1680+
required: true
1681+
default: 'Hello'
1682+
entryPoint: # id of input
1683+
description: 'optional docker entrypoint overwrite.'
1684+
required: false
1685+
outputs:
1686+
time: # id of output
1687+
description: 'The time we did the greeting'
1688+
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
1689+
color: 'green' # optional, decorates the entry in the GitHub Marketplace
1690+
runs:
1691+
using: 'node24'
1692+
main: 'task.js'
1693+
";
1694+
Pipelines.ActionStep instance;
1695+
string directory;
1696+
CreateAction(yamlContent: Content, instance: out instance, directory: out directory);
1697+
1698+
// Act.
1699+
Definition definition = _actionManager.LoadAction(_ec.Object, instance);
1700+
1701+
// Assert.
1702+
Assert.NotNull(definition);
1703+
Assert.Equal(directory, definition.Directory);
1704+
Assert.NotNull(definition.Data);
1705+
Assert.NotNull(definition.Data.Inputs); // inputs
1706+
Dictionary<string, string> inputDefaults = new(StringComparer.OrdinalIgnoreCase);
1707+
foreach (var input in definition.Data.Inputs)
1708+
{
1709+
var name = input.Key.AssertString("key").Value;
1710+
var value = input.Value.AssertScalar("value").ToString();
1711+
1712+
_hc.GetTrace().Info($"Default: {name} = {value}");
1713+
inputDefaults[name] = value;
1714+
}
1715+
1716+
Assert.Equal(2, inputDefaults.Count);
1717+
Assert.True(inputDefaults.ContainsKey("greeting"));
1718+
Assert.Equal("Hello", inputDefaults["greeting"]);
1719+
Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"]));
1720+
Assert.NotNull(definition.Data.Execution); // execution
1721+
1722+
Assert.NotNull(definition.Data.Execution as NodeJSActionExecutionData);
1723+
Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script);
1724+
Assert.Equal("node24", (definition.Data.Execution as NodeJSActionExecutionData).NodeVersion);
1725+
}
1726+
finally
1727+
{
1728+
Teardown();
1729+
}
1730+
}
1731+
16621732
[Fact]
16631733
[Trait("Level", "L0")]
16641734
[Trait("Category", "Worker")]

src/Test/L0/Worker/ActionManifestManagerL0.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,49 @@ public void Load_Node20Action()
502502
}
503503
}
504504

505+
[Fact]
506+
[Trait("Level", "L0")]
507+
[Trait("Category", "Worker")]
508+
public void Load_Node24Action()
509+
{
510+
try
511+
{
512+
//Arrange
513+
Setup();
514+
515+
var actionManifest = new ActionManifestManager();
516+
actionManifest.Initialize(_hc);
517+
518+
//Act
519+
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node24action.yml"));
520+
521+
//Assert
522+
Assert.Equal("Hello World", result.Name);
523+
Assert.Equal("Greet the world and record the time", result.Description);
524+
Assert.Equal(2, result.Inputs.Count);
525+
Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value);
526+
Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value);
527+
Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value);
528+
Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value);
529+
Assert.Equal(1, result.Deprecated.Count);
530+
531+
Assert.True(result.Deprecated.ContainsKey("greeting"));
532+
result.Deprecated.TryGetValue("greeting", out string value);
533+
Assert.Equal("This property has been deprecated", value);
534+
535+
Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType);
536+
537+
var nodeAction = result.Execution as NodeJSActionExecutionData;
538+
539+
Assert.Equal("main.js", nodeAction.Script);
540+
Assert.Equal("node24", nodeAction.NodeVersion);
541+
}
542+
finally
543+
{
544+
Teardown();
545+
}
546+
}
547+
505548
[Fact]
506549
[Trait("Level", "L0")]
507550
[Trait("Category", "Worker")]
@@ -758,7 +801,7 @@ public void Load_CompositeActionNoUsing()
758801
//Assert
759802
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
760803
Assert.Contains($"Failed to load {action_path}", err.Message);
761-
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
804+
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
762805
}
763806
finally
764807
{

0 commit comments

Comments
 (0)