Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7ce5919
node 24 initial
salmanmkc Jul 11, 2025
d43638b
apline update
salmanmkc Jul 11, 2025
e3619ca
remove apline≠
salmanmkc Jul 11, 2025
0af6876
remove line
salmanmkc Jul 11, 2025
f83aab0
remove these calls
salmanmkc Jul 11, 2025
b15643d
update since linux arm doesn't have 32 bit
salmanmkc Jul 11, 2025
a34f756
mkdir if directory doesn't exist
salmanmkc Jul 11, 2025
922a35b
add comment
salmanmkc Jul 11, 2025
ea5b865
Update to use the latest version of alpine that was just published an…
salmanmkc Jul 11, 2025
2418b74
Feature flag node 24
salmanmkc Jul 15, 2025
6a21e10
add tests for env variables
salmanmkc Jul 15, 2025
64b51eb
test update
salmanmkc Jul 15, 2025
0e7cd9e
Refactor Node version handling to remove feature flag checks for Node…
salmanmkc Jul 16, 2025
f06af33
Remove ff and internal node usage. Warning for linux 32bit arm since …
salmanmkc Jul 16, 2025
219a696
remove warning checks
salmanmkc Jul 16, 2025
7a5e49a
Rename method (linuxarm32)
salmanmkc Jul 16, 2025
775dff8
Merge branch 'main' into salmanmkc/1-node24
salmanmkc Jul 16, 2025
e46d4d0
remove file
salmanmkc Jul 16, 2025
a4727b1
Merge branch 'salmanmkc/1-node24' of https://github.com/actions/runne…
salmanmkc Jul 16, 2025
c4dfeba
Refactor Node version compatibility checks to NodeUtil and update tests
salmanmkc Jul 16, 2025
2d64c31
Merge branch 'main' into salmanmkc/1-node24 (null ref fix)
salmanmkc Jul 17, 2025
47c19eb
remove this using that is not used
salmanmkc Jul 17, 2025
4a15294
remove file
salmanmkc Jul 17, 2025
c270930
remove empty lines
salmanmkc Jul 17, 2025
57855c4
equals instead of starts with and remove imports that are not needed
salmanmkc Jul 17, 2025
d5ae14d
Remove if statement nesting
salmanmkc Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/checks/nodejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

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

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

All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node20/`.
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.

> Not the `node` from `$PATH`

Expand Down
15 changes: 15 additions & 0 deletions src/Misc/externals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE20_VERSION="20.19.3"
NODE24_VERSION="24.4.0"

get_abs_path() {
# exploits the fact that pwd will print abs path when no args
Expand Down Expand Up @@ -139,6 +140,8 @@ function acquireExternalTool() {
if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
if [[ "$PRECACHE" != "" ]]; then
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
fi
Expand All @@ -149,6 +152,8 @@ if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
# todo: replace these with official release when available
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
if [[ "$PRECACHE" != "" ]]; then
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
fi
Expand All @@ -157,23 +162,33 @@ fi
# Download the external tools only for OSX.
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-x64.tar.gz" node24 fix_nested_dir
fi

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

# Download the external tools for Linux PACKAGERUNTIMEs.
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-x64.tar.gz" node20 fix_nested_dir
acquireExternalTool "$NODE_ALPINE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-x64.tar.gz" node24 fix_nested_dir
# Use v24.3.0 for alpine as it's the latest available in actions/alpine_nodejs
acquireExternalTool "$NODE_ALPINE_URL/v24.3.0/node-v24.3.0-alpine-x64.tar.gz" node24_alpine
fi

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

if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-armv7l.tar.gz" node20 fix_nested_dir
# Node.js 24 doesn't provide official armv7l builds
# Using Node 20 as a fallback for Node 24 on linux-arm
mkdir -p "$LAYOUT_DIR/externals/node24" || checkRC 'mkdir'
ln -sf ../node20 "$LAYOUT_DIR/externals/node24"
fi
11 changes: 8 additions & 3 deletions src/Misc/layoutbin/update.sh.template
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,17 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
then
# inspect the open file handles to find the node process
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
nodever="node20"
# Try finding node24 first, then fallback to earlier versions if needed
nodever="node24"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node20
then
nodever="node16"
nodever="node20"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
then
nodever="node16"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
Copy link
Member

Choose a reason for hiding this comment

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

i think we missed an fi after this line...

if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
then
nodever="node12"
Expand Down
2 changes: 1 addition & 1 deletion src/Runner.Common/Util/NodeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace GitHub.Runner.Common.Util
public static class NodeUtil
{
private const string _defaultNodeVersion = "node20";
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20", "node24" });
public static string GetInternalNodeVersion()
{
var forcedInternalNodeVersion = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion);
Expand Down
7 changes: 4 additions & 3 deletions src/Runner.Worker/ActionManifestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@ private ActionExecutionData ConvertRuns(
}
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase))
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(mainToken?.Value))
{
Expand Down Expand Up @@ -490,7 +491,7 @@ private ActionExecutionData ConvertRuns(
}
else
{
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16' or 'node20' instead.");
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
}
}
else if (pluginToken != null)
Expand All @@ -501,7 +502,7 @@ private ActionExecutionData ConvertRuns(
};
}

throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.");
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
}

private void ConvertInputs(
Expand Down
71 changes: 71 additions & 0 deletions src/Test/L0/Worker/ActionManagerL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,77 @@ public void LoadsNode20ActionDefinition()
Teardown();
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void LoadsNode24ActionDefinition()
{
try
{
// Arrange.
Setup();
const string Content = @"
# Container action
name: 'Hello World'
description: 'Greet the world and record the time'
author: 'GitHub'
inputs:
greeting: # id of input
description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout'
required: true
default: 'Hello'
entryPoint: # id of input
description: 'optional docker entrypoint overwrite.'
required: false
outputs:
time: # id of output
description: 'The time we did the greeting'
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
color: 'green' # optional, decorates the entry in the GitHub Marketplace
runs:
using: 'node24'
main: 'task.js'
";
Pipelines.ActionStep instance;
string directory;
CreateAction(yamlContent: Content, instance: out instance, directory: out directory);

// Act.
Definition definition = _actionManager.LoadAction(_ec.Object, instance);

// Assert.
Assert.NotNull(definition);
Assert.Equal(directory, definition.Directory);
Assert.NotNull(definition.Data);
Assert.NotNull(definition.Data.Inputs); // inputs
Dictionary<string, string> inputDefaults = new(StringComparer.OrdinalIgnoreCase);
foreach (var input in definition.Data.Inputs)
{
var name = input.Key.AssertString("key").Value;
var value = input.Value.AssertScalar("value").ToString();

_hc.GetTrace().Info($"Default: {name} = {value}");
inputDefaults[name] = value;
}

Assert.Equal(2, inputDefaults.Count);
Assert.True(inputDefaults.ContainsKey("greeting"));
Assert.Equal("Hello", inputDefaults["greeting"]);
Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"]));
Assert.NotNull(definition.Data.Execution); // execution

Assert.NotNull(definition.Data.Execution as NodeJSActionExecutionData);
Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script);
Assert.Equal("node24", (definition.Data.Execution as NodeJSActionExecutionData).NodeVersion);
}
finally
{
Teardown();
}
}


[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
Expand Down
45 changes: 44 additions & 1 deletion src/Test/L0/Worker/ActionManifestManagerL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,49 @@ public void Load_Node20Action()
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_Node24Action()
{
try
{
//Arrange
Setup();

var actionManifest = new ActionManifestManager();
actionManifest.Initialize(_hc);

//Act
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node24action.yml"));

//Assert
Assert.Equal("Hello World", result.Name);
Assert.Equal("Greet the world and record the time", result.Description);
Assert.Equal(2, result.Inputs.Count);
Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value);
Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value);
Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value);
Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value);
Assert.Equal(1, result.Deprecated.Count);

Assert.True(result.Deprecated.ContainsKey("greeting"));
result.Deprecated.TryGetValue("greeting", out string value);
Assert.Equal("This property has been deprecated", value);

Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType);

var nodeAction = result.Execution as NodeJSActionExecutionData;

Assert.Equal("main.js", nodeAction.Script);
Assert.Equal("node24", nodeAction.NodeVersion);
}
finally
{
Teardown();
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
Expand Down Expand Up @@ -758,7 +801,7 @@ public void Load_CompositeActionNoUsing()
//Assert
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
Assert.Contains($"Failed to load {action_path}", err.Message);
_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);
_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);
}
finally
{
Expand Down
1 change: 1 addition & 0 deletions src/Test/L0/Worker/HandlerFactoryL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private TestHostContext CreateTestContext([CallerMemberName] string testName = "
[InlineData("node12", "node20")]
[InlineData("node16", "node20")]
[InlineData("node20", "node20")]
[InlineData("node24", "node24")]
public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion)
{
using (TestHostContext hc = CreateTestContext())
Expand Down
35 changes: 35 additions & 0 deletions src/Test/L0/Worker/Handlers/NodeHandlerL0.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;

namespace GitHub.Runner.Common.Tests.Worker.Handlers
{
public sealed class NodeHandlerL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void NodeJSActionExecutionDataSupportsNode24()
{
// Create NodeJSActionExecutionData with node24
var nodeJSData = new NodeJSActionExecutionData
{
NodeVersion = "node24",
Script = "test.js"
};

// Act & Assert
Assert.Equal("node24", nodeJSData.NodeVersion);
Assert.Equal(ActionExecutionType.NodeJS, nodeJSData.ExecutionType);
}
}
}
54 changes: 54 additions & 0 deletions src/Test/L0/Worker/StepHostL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,60 @@ public async Task DetermineNode20RuntimeVersionInUnknowContainerAsync()
Assert.Equal("node20", nodeVersion);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var sh = new ContainerStepHost();
sh.Initialize(hc);
sh.Container = new ContainerInfo() { ContainerId = "1234abcd" };

_dc.Setup(d => d.DockerExec(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<List<string>>()))
.Callback((IExecutionContext ec, string id, string options, string command, List<string> output) =>
{
output.Add("alpine");
})
.ReturnsAsync(0);

// Act.
var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node24");

// Assert.
Assert.Equal("node24_alpine", nodeVersion);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DetermineNode24RuntimeVersionInUnknownContainerAsync()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var sh = new ContainerStepHost();
sh.Initialize(hc);
sh.Container = new ContainerInfo() { ContainerId = "1234abcd" };

_dc.Setup(d => d.DockerExec(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<List<string>>()))
.Callback((IExecutionContext ec, string id, string options, string command, List<string> output) =>
{
output.Add("github");
})
.ReturnsAsync(0);

// Act.
var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node24");

// Assert.
Assert.Equal("node24", nodeVersion);
}
}
#endif
}
}
20 changes: 20 additions & 0 deletions src/Test/TestData/node24action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Hello World'
description: 'Greet the world and record the time'
author: 'Test Corporation'
inputs:
greeting: # id of input
description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout'
required: true
default: 'Hello'
deprecationMessage: 'This property has been deprecated'
entryPoint: # id of input
description: 'optional docker entrypoint overwrite.'
required: false
outputs:
time: # id of output
description: 'The time we did the greeting'
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
color: 'green' # optional, decorates the entry in the GitHub Marketplace
runs:
using: 'node24'
main: 'main.js'
Loading