diff --git a/src/Misc/contentHash/externals/linux-arm b/src/Misc/contentHash/externals/linux-arm index 6b7b0e285..20eec3493 100644 --- a/src/Misc/contentHash/externals/linux-arm +++ b/src/Misc/contentHash/externals/linux-arm @@ -1 +1 @@ -209a3b07522d883b745945ec8b1bfa57c0bce769573c3e79206b62959686c0d3 \ No newline at end of file +5bdddd32bab1e57af252b470579083049496e9e39b6e4f50de01232581f9a2d8 \ No newline at end of file diff --git a/src/Misc/contentHash/externals/linux-arm64 b/src/Misc/contentHash/externals/linux-arm64 index 5793cdfec..427850748 100644 --- a/src/Misc/contentHash/externals/linux-arm64 +++ b/src/Misc/contentHash/externals/linux-arm64 @@ -1 +1 @@ -1dd7654995ab54ce0d06519ad84c25029f0924f8313f0f5b7811c3631982a5e5 \ No newline at end of file +54b3b3a72da93db0fa38708c759fceadddb70cacdd3620a079084a242126dd78 \ No newline at end of file diff --git a/src/Misc/contentHash/externals/linux-x64 b/src/Misc/contentHash/externals/linux-x64 index f940a46b5..5e39ae85c 100644 --- a/src/Misc/contentHash/externals/linux-x64 +++ b/src/Misc/contentHash/externals/linux-x64 @@ -1 +1 @@ -9a06b2be3c18a06d2e676774d8b6d2869854f23f982433e9346bc7bdcb08f362 \ No newline at end of file +e7f2da271abb174285c3a757503538b3e9792e9d731b0382b6d1f21bb59a79ba \ No newline at end of file diff --git a/src/Misc/contentHash/externals/osx-arm64 b/src/Misc/contentHash/externals/osx-arm64 index aa71ab0a0..678ad1a4f 100644 --- a/src/Misc/contentHash/externals/osx-arm64 +++ b/src/Misc/contentHash/externals/osx-arm64 @@ -1 +1 @@ -4eb04e10e173b1f5d7cf8890878a3e178de162e5bf1a583067cf704b8162631a \ No newline at end of file +2481c5b0d06b2b5621635f2568b86a43b0e5b259fed1298167ba4f33d4c464c7 \ No newline at end of file diff --git a/src/Misc/contentHash/externals/osx-x64 b/src/Misc/contentHash/externals/osx-x64 index 4d9d14957..52123e90c 100644 --- a/src/Misc/contentHash/externals/osx-x64 +++ b/src/Misc/contentHash/externals/osx-x64 @@ -1 +1 @@ -fc5efd1df9c75f01d7b6a43018ba49257bdc0d65e743ddd1ae753802a5ddb736 +85de7677165e65ec69b8a9e344c0811efa51b7fe5376a1aa083505c560ea6f57 \ No newline at end of file diff --git a/src/Misc/contentHash/externals/win-arm64 b/src/Misc/contentHash/externals/win-arm64 index 7a0d7a382..b0f866616 100644 --- a/src/Misc/contentHash/externals/win-arm64 +++ b/src/Misc/contentHash/externals/win-arm64 @@ -1 +1 @@ -0064acac3fba3bd29851334c90f53adc13f88f2d23f5909f3bbec2b1c4ade9ef +763d18de11c11fd299c0e75e98fefc8a0e6605ae0ad6aba3bbc110db2262ab41 \ No newline at end of file diff --git a/src/Misc/contentHash/externals/win-x64 b/src/Misc/contentHash/externals/win-x64 index 4948aeb12..aa8b56b59 100644 --- a/src/Misc/contentHash/externals/win-x64 +++ b/src/Misc/contentHash/externals/win-x64 @@ -1 +1 @@ -e2fa2229ed5bc8857acdd264c2f3964454fd710c776efdfb5dc0b40b7a75065a \ No newline at end of file +16f3cc545dfe10e84df43746073fc64d3c44d1891782532805aeb2118869a55d \ No newline at end of file diff --git a/src/Misc/externals.sh b/src/Misc/externals.sh index 0d7853b97..0aac70570 100755 --- a/src/Misc/externals.sh +++ b/src/Misc/externals.sh @@ -5,6 +5,7 @@ PRECACHE=$2 NODE_URL=https://nodejs.org/dist UNOFFICIAL_NODE_URL=https://unofficial-builds.nodejs.org/download/release NODE16_VERSION="16.20.1" +NODE20_VERSION="20.5.0" # used only for win-arm64, remove node16 unofficial version when official version is available NODE16_UNOFFICIAL_VERSION="16.20.0" @@ -140,6 +141,8 @@ function acquireExternalTool() { if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/$PACKAGERUNTIME/node.exe" node16/bin acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/$PACKAGERUNTIME/node.lib" node16/bin + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin if [[ "$PRECACHE" != "" ]]; then acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere fi @@ -150,6 +153,8 @@ if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then # todo: replace these with official release when available acquireExternalTool "$UNOFFICIAL_NODE_URL/v${NODE16_UNOFFICIAL_VERSION}/$PACKAGERUNTIME/node.exe" node16/bin acquireExternalTool "$UNOFFICIAL_NODE_URL/v${NODE16_UNOFFICIAL_VERSION}/$PACKAGERUNTIME/node.lib" node16/bin + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin if [[ "$PRECACHE" != "" ]]; then acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere fi @@ -158,23 +163,29 @@ fi # Download the external tools only for OSX. if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-darwin-x64.tar.gz" node16 fix_nested_dir + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir fi if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then # node.js v12 doesn't support macOS on arm64. acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-darwin-arm64.tar.gz" node16 fix_nested_dir + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-arm64.tar.gz" node20 fix_nested_dir fi # Download the external tools for Linux PACKAGERUNTIMEs. if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-x64.tar.gz" node16 fix_nested_dir acquireExternalTool "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE16_VERSION}/alpine/x64/node-v${NODE16_VERSION}-alpine-x64.tar.gz" node16_alpine + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-x64.tar.gz" node20 fix_nested_dir + acquireExternalTool "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE20_VERSION}/alpine/x64/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine fi if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-arm64.tar.gz" node16 fix_nested_dir + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-arm64.tar.gz" node20 fix_nested_dir fi if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-armv7l.tar.gz" node16 fix_nested_dir + acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-armv7l.tar.gz" node20 fix_nested_dir fi diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index e39031cba..f2c01d7d3 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -6,8 +6,7 @@ namespace GitHub.Runner.Common.Util public static class NodeUtil { private const string _defaultNodeVersion = "node16"; - public static readonly ReadOnlyCollection BuiltInNodeVersions = new(new[] { "node16" }); - + public static readonly ReadOnlyCollection BuiltInNodeVersions = new(new[] { "node16", "node20" }); public static string GetInternalNodeVersion() { var forcedInternalNodeVersion = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion); diff --git a/src/Runner.Worker/ActionManifestManager.cs b/src/Runner.Worker/ActionManifestManager.cs index 31ea708d8..6b46e6a4e 100644 --- a/src/Runner.Worker/ActionManifestManager.cs +++ b/src/Runner.Worker/ActionManifestManager.cs @@ -449,7 +449,8 @@ namespace GitHub.Runner.Worker } } else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) || - string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase)) + string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) || + string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(mainToken?.Value)) { @@ -489,7 +490,7 @@ namespace GitHub.Runner.Worker } else { - throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12' or 'node16' instead."); + throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16' or 'node20' instead."); } } else if (pluginToken != null) @@ -500,7 +501,7 @@ namespace GitHub.Runner.Worker }; } - throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12' or 'node16'."); + throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'."); } private void ConvertInputs( diff --git a/src/Runner.Worker/Handlers/HandlerFactory.cs b/src/Runner.Worker/Handlers/HandlerFactory.cs index 0ef666905..5f1fce0cf 100644 --- a/src/Runner.Worker/Handlers/HandlerFactory.cs +++ b/src/Runner.Worker/Handlers/HandlerFactory.cs @@ -69,7 +69,7 @@ namespace GitHub.Runner.Worker.Handlers warningActions = StringUtil.ConvertFromJson>(node16ForceWarnings); } - var repoActionFullName = ""; + string repoActionFullName; if (string.IsNullOrEmpty(repoAction.Name)) { repoActionFullName = repoAction.Path; // local actions don't have a 'Name' diff --git a/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs b/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs index 2388ee022..c5bc7fd6e 100644 --- a/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs +++ b/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs @@ -108,8 +108,8 @@ namespace GitHub.Runner.Worker.Handlers ExecutionContext.Output($"The node12 is not supported. Use node16 instead."); Data.NodeVersion = "node16"; } - string forcedNodeVersion = System.Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedActionsNodeVersion); + string forcedNodeVersion = System.Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedActionsNodeVersion); if (forcedNodeVersion == "node16" && Data.NodeVersion != "node16") { Data.NodeVersion = "node16"; diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index cd93cd8c0..a3c45351d 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -1424,6 +1424,74 @@ runs: } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsNode20ActionDefinition() + { + 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: 'node20' + 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 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("node20", (definition.Data.Execution as NodeJSActionExecutionData).NodeVersion); + } + finally + { + Teardown(); + } + } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] diff --git a/src/Test/L0/Worker/ActionManifestManagerL0.cs b/src/Test/L0/Worker/ActionManifestManagerL0.cs index 3ad54a543..385ae9463 100644 --- a/src/Test/L0/Worker/ActionManifestManagerL0.cs +++ b/src/Test/L0/Worker/ActionManifestManagerL0.cs @@ -459,6 +459,49 @@ namespace GitHub.Runner.Common.Tests.Worker } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_Node20Action() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManager(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node20action.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("node20", nodeAction.NodeVersion); + } + finally + { + Teardown(); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -715,7 +758,7 @@ namespace GitHub.Runner.Common.Tests.Worker //Assert var err = Assert.Throws(() => actionManifest.Load(_ec.Object, action_path)); Assert.Contains($"Fail to load {action_path}", err.Message); - _ec.Verify(x => x.AddIssue(It.Is(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12' or 'node16'.")), It.IsAny()), Times.Once); + _ec.Verify(x => x.AddIssue(It.Is(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.")), It.IsAny()), Times.Once); } finally { diff --git a/src/Test/L0/Worker/HandlerFactoryL0.cs b/src/Test/L0/Worker/HandlerFactoryL0.cs index ce5fb5d81..0a9552d99 100644 --- a/src/Test/L0/Worker/HandlerFactoryL0.cs +++ b/src/Test/L0/Worker/HandlerFactoryL0.cs @@ -32,6 +32,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] [InlineData("node12", "node16")] [InlineData("node16", "node16")] + [InlineData("node20", "node20")] public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion) { using (TestHostContext hc = CreateTestContext()) diff --git a/src/Test/L0/Worker/StepHostL0.cs b/src/Test/L0/Worker/StepHostL0.cs index f39c0e879..f6b58890c 100644 --- a/src/Test/L0/Worker/StepHostL0.cs +++ b/src/Test/L0/Worker/StepHostL0.cs @@ -82,6 +82,33 @@ namespace GitHub.Runner.Common.Tests.Worker } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task DetermineNode20RuntimeVersionInAlpineContainerAsync() + { + 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(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback((IExecutionContext ec, string id, string options, string command, List output) => + { + output.Add("alpine"); + }) + .ReturnsAsync(0); + + // Act. + var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node20"); + + // Assert. + Assert.Equal("node20_alpine", nodeVersion); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -108,6 +135,33 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal("node16", nodeVersion); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task DetermineNode20RuntimeVersionInUnknowContainerAsync() + { + 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(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback((IExecutionContext ec, string id, string options, string command, List output) => + { + output.Add("github"); + }) + .ReturnsAsync(0); + + // Act. + var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node20"); + + // Assert. + Assert.Equal("node20", nodeVersion); + } + } #endif } } diff --git a/src/Test/TestData/node20action.yml b/src/Test/TestData/node20action.yml new file mode 100644 index 000000000..88870dbbc --- /dev/null +++ b/src/Test/TestData/node20action.yml @@ -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: 'node20' + main: 'main.js' \ No newline at end of file