Compare commits

...

17 Commits

Author SHA1 Message Date
Salman Chishti
820bcaf258 Update generic methdod to shelcheck all files that can be 2025-07-29 07:18:21 +00:00
Salman Chishti
1f9418d6c0 update 2025-07-28 13:08:39 +00:00
Salman Chishti
49b30c8a23 add skip back for windows 2025-07-28 13:01:04 +00:00
Salman Chishti
577c73ee80 don't skip on windows 2025-07-28 12:34:13 +00:00
Salman Chishti
2dc3c3adc1 Enhance shell script validation with detailed debugging and ShellCheck integration 2025-07-28 12:08:46 +00:00
Salman Chishti
ffc2086972 skip win 64 2025-07-27 22:07:16 +00:00
Salman Chishti
63ada10762 update 2025-07-27 21:54:57 +00:00
Salman Chishti
c824407a9b better validations 2025-07-27 21:48:52 +00:00
Salman Chishti
f06a45283d update with temp file 2025-07-27 21:38:44 +00:00
Salman Chishti
3aef228adc Add tests for making sure that template files have no syntax errors 2025-07-27 21:24:45 +00:00
Salman Chishti
6ca97eeb88 Fix if statement structure in update script and variable reference (#3956) 2025-07-25 16:42:28 +01:00
github-actions[bot]
8a9b96806d Update Docker to v28.3.2 and Buildx to v0.26.1 (#3953)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-23 22:41:02 -04:00
Salman Chishti
dc9cf684c9 Prepare runner release 2.327.0 (#3951) 2025-07-22 18:59:15 +01:00
github-actions[bot]
c765c990b9 Update dotnet sdk to latest version @8.0.412 (#3941)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-22 11:32:03 -04:00
Salman Chishti
ed48ddd08c Runner Support for executing Node24 Actions (#3940) 2025-07-17 01:00:17 +00:00
Salman Chishti
a1e6ad8d2e Fix null reference exception in user agent handling (#3946) 2025-07-17 01:12:03 +01:00
Tingluo Huang
14856e63bc Try add orchestrationid into user-agent using token claim. (#3945) 2025-07-16 14:11:09 -04:00
23 changed files with 1247 additions and 37 deletions

View File

@@ -4,7 +4,7 @@
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
"ghcr.io/devcontainers/features/dotnet": {
"version": "8.0.411"
"version": "8.0.412"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"

View File

@@ -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`

View File

@@ -5,8 +5,8 @@ ARG TARGETOS
ARG TARGETARCH
ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
ARG DOCKER_VERSION=28.3.0
ARG BUILDX_VERSION=0.25.0
ARG DOCKER_VERSION=28.3.2
ARG BUILDX_VERSION=0.26.1
RUN apt update -y && apt install curl unzip -y

View File

@@ -1,15 +1,13 @@
## What's Changed
* runner timestamps invariant by @GhadimiR in https://github.com/actions/runner/pull/3888
* Update README.md by @nebuk89 in https://github.com/actions/runner/pull/3898
* Update dotnet sdk to latest version @8.0.411 by @github-actions in https://github.com/actions/runner/pull/3911
* Update Docker to v28.2.2 and Buildx to v0.25.0 by @github-actions in https://github.com/actions/runner/pull/3918
* Bump windows service app to dotnet 4.7 by @TingluoHuang in https://github.com/actions/runner/pull/3926
* Upgrade node.js to latest version. by @TingluoHuang in https://github.com/actions/runner/pull/3935
* Try add orchestrationid into user-agent using token claim. by @TingluoHuang in https://github.com/actions/runner/pull/3945
* Fix null reference exception in user agent handling by @salmanmkc in https://github.com/actions/runner/pull/3946
* Runner Support for executing Node24 Actions by @salmanmkc in https://github.com/actions/runner/pull/3940
* Update dotnet sdk to latest version @8.0.412 by @github-actions[bot] in https://github.com/actions/runner/pull/3941
## New Contributors
* @nebuk89 made their first contribution in https://github.com/actions/runner/pull/3898
* @salmanmkc made their first contribution in https://github.com/actions/runner/pull/3946
**Full Changelog**: https://github.com/actions/runner/compare/v2.325.0...v2.326.0
**Full Changelog**: https://github.com/actions/runner/compare/v2.326.0...v2.327.0
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

@@ -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
@@ -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
@@ -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
@@ -157,21 +162,26 @@ 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
acquireExternalTool "$NODE_ALPINE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-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

View File

@@ -123,7 +123,8 @@ fi
# fix upgrade issue with macOS when running as a service
attemptedtargetedfix=0
currentplatform=$(uname | awk '{print tolower($0)}')
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
if [[ "$currentplatform" == 'darwin' && $restartinteractiverunner -eq 0 ]];
then
# We needed a fix for https://github.com/actions/runner/issues/743
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
@@ -135,6 +136,11 @@ 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
# 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 node20
then
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
@@ -147,6 +153,7 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
fi
fi
fi
if [[ $? -eq 0 && -n "$path" ]]
then
# trim the last 5 characters of the path '/node'

View File

@@ -15,6 +15,7 @@ using System.Threading.Tasks;
using GitHub.DistributedTask.Logging;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Services.WebApi.Jwt;
namespace GitHub.Runner.Common
{
@@ -306,6 +307,36 @@ namespace GitHub.Runner.Common
{
_userAgents.Add(new ProductInfoHeaderValue("ClientId", clientId));
}
// for Hosted runner, we can pull orchestrationId from JWT claims of the runner listening token.
if (credData != null &&
credData.Scheme == Constants.Configuration.OAuthAccessToken &&
credData.Data.TryGetValue(Constants.Runner.CommandLine.Args.Token, out var accessToken) &&
!string.IsNullOrEmpty(accessToken))
{
try
{
var jwt = JsonWebToken.Create(accessToken);
var claims = jwt.ExtractClaims();
var orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orch_id", StringComparison.OrdinalIgnoreCase))?.Value;
if (string.IsNullOrEmpty(orchestrationId))
{
// fallback to orchid for C# actions-service
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
}
if (!string.IsNullOrEmpty(orchestrationId))
{
_trace.Info($"Pull OrchestrationId {orchestrationId} from runner JWT claims");
_userAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId));
}
}
catch (Exception ex)
{
_trace.Error("Fail to extract OrchestrationId from runner JWT claims");
_trace.Error(ex);
}
}
}
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);

View File

@@ -18,5 +18,22 @@ namespace GitHub.Runner.Common.Util
}
return _defaultNodeVersion;
}
/// <summary>
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
/// </summary>
/// <param name="preferredVersion">The preferred Node version</param>
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
{
if (string.Equals(preferredVersion, "node24", StringComparison.OrdinalIgnoreCase) &&
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
{
return ("node20", "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
}
return (preferredVersion, null);
}
}
}

View File

@@ -110,7 +110,12 @@ namespace GitHub.Runner.Listener
{
var jwt = JsonWebToken.Create(accessToken);
var claims = jwt.ExtractClaims();
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orch_id", StringComparison.OrdinalIgnoreCase))?.Value;
if (string.IsNullOrEmpty(orchestrationId))
{
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
}
if (!string.IsNullOrEmpty(orchestrationId))
{
Trace.Info($"Pull OrchestrationId {orchestrationId} from JWT claims");

View File

@@ -450,7 +450,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, "node20", StringComparison.OrdinalIgnoreCase))
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(mainToken?.Value))
{
@@ -490,7 +491,7 @@ namespace GitHub.Runner.Worker
}
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)
@@ -501,7 +502,7 @@ namespace GitHub.Runner.Worker
};
}
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(

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using GitHub.DistributedTask.Pipelines.ContextData;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -9,7 +8,6 @@ using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Linq;
using GitHub.Runner.Worker.Container.ContainerHooks;
using System.IO;
using System.Threading.Channels;
namespace GitHub.Runner.Worker.Handlers
@@ -60,7 +58,14 @@ namespace GitHub.Runner.Worker.Handlers
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{
return Task.FromResult<string>(preferredVersion);
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
return Task.FromResult(nodeVersion);
}
public async Task<int> ExecuteAsync(IExecutionContext context,
@@ -137,8 +142,12 @@ namespace GitHub.Runner.Worker.Handlers
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{
// Optimistically use the default
string nodeExternal = preferredVersion;
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
@@ -264,7 +273,14 @@ namespace GitHub.Runner.Worker.Handlers
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
{
string nodeExternal = preferredVersion;
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
// Check for Alpine container compatibility
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
{
var os = Constants.Runner.Platform.ToString();

View File

@@ -49,9 +49,12 @@ namespace GitHub.Runner.Worker
// add orchestration id to useragent for better correlation.
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
!string.IsNullOrEmpty(orchestrationId.Value))
{
if (!HostContext.UserAgents.Any(x => string.Equals(x.Product?.Name, "OrchestrationId", StringComparison.OrdinalIgnoreCase)))
{
// make the orchestration id the first item in the user-agent header to avoid get truncated in server log.
HostContext.UserAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
}
// make sure orchestration id is in the user-agent header.
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);

View File

@@ -0,0 +1,793 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Linq;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Sdk;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener
{
public sealed class ShellScriptSyntaxL0
{
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null, bool useFullPath = false, bool useShellCheck = true)
{
// Skip on Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string templatePath;
if (useFullPath)
{
templatePath = templateName;
}
else
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
templatePath = Path.Combine(rootDirectory, relativePath, templateName);
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string tempScriptPath = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(templateName));
string debugLogPath = Path.Combine(tempDir, "debug_log.txt");
string template = File.ReadAllText(templatePath);
if (templateModifier != null)
{
template = templateModifier(template);
}
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
template = ReplaceCommonPlaceholders(template, rootFolder, tempDir);
File.WriteAllText(tempScriptPath, template);
var chmodProcess = new Process();
chmodProcess.StartInfo.FileName = "chmod";
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
chmodProcess.Start();
chmodProcess.WaitForExit();
var bashCheckProcess = new Process();
bashCheckProcess.StartInfo.FileName = "/bin/bash";
bashCheckProcess.StartInfo.Arguments = $"-c \"bash -n {tempScriptPath}; echo $?\"";
bashCheckProcess.StartInfo.RedirectStandardOutput = true;
bashCheckProcess.StartInfo.RedirectStandardError = true;
bashCheckProcess.StartInfo.UseShellExecute = false;
bashCheckProcess.Start();
string bashCheckOutput = bashCheckProcess.StandardOutput.ReadToEnd();
string bashCheckErrors = bashCheckProcess.StandardError.ReadToEnd();
bashCheckProcess.WaitForExit();
// Act - Check syntax using bash -n
var process = new Process();
process.StartInfo.FileName = "bash";
process.StartInfo.Arguments = $"-n {tempScriptPath}";
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.Start();
string errors = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(errors))
{
Console.WriteLine($"Errors: {errors}");
}
// Assert based on expected outcome
if (shouldPass)
{
Console.WriteLine("Test expected to pass, checking exit code and errors");
Assert.Equal(0, process.ExitCode);
Assert.Empty(errors);
if (shouldPass && process.ExitCode == 0 && useShellCheck)
{
RunShellCheck(tempScriptPath);
}
}
else
{
Console.WriteLine("Test expected to fail, checking exit code and errors");
Assert.NotEqual(0, process.ExitCode);
Assert.NotEmpty(errors);
}
// Cleanup - But leave the temp directory for debugging on failure
if (process.ExitCode == 0 && shouldPass)
{
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
else
{
Console.WriteLine($"Not cleaning up temp directory for debugging: {tempDir}");
}
}
}
catch (Exception ex)
{
Assert.Fail($"Exception during test for {templateName}: {ex}");
}
}
private void RunShellCheck(string scriptPath)
{
var shellcheckExistsProcess = new Process();
shellcheckExistsProcess.StartInfo.FileName = "which";
shellcheckExistsProcess.StartInfo.Arguments = "shellcheck";
shellcheckExistsProcess.StartInfo.RedirectStandardOutput = true;
shellcheckExistsProcess.StartInfo.UseShellExecute = false;
shellcheckExistsProcess.Start();
string shellcheckPath = shellcheckExistsProcess.StandardOutput.ReadToEnd().Trim();
shellcheckExistsProcess.WaitForExit();
if (!string.IsNullOrEmpty(shellcheckPath))
{
Console.WriteLine("ShellCheck found, performing additional validation");
var shellcheckProcess = new Process();
shellcheckProcess.StartInfo.FileName = "shellcheck";
shellcheckProcess.StartInfo.Arguments = $"-e SC2001,SC2002,SC2006,SC2009,SC2016,SC2034,SC2039,SC2046,SC2048,SC2059,SC2086,SC2094,SC2115,SC2116,SC2126,SC2129,SC2140,SC2145,SC2153,SC2154,SC2155,SC2162,SC2164,SC2166,SC2174,SC2181,SC2206,SC2207,SC2221,SC2222,SC2230,SC2236,SC2242,SC2268 {scriptPath}";
shellcheckProcess.StartInfo.RedirectStandardOutput = true;
shellcheckProcess.StartInfo.RedirectStandardError = true;
shellcheckProcess.StartInfo.UseShellExecute = false;
shellcheckProcess.Start();
string shellcheckOutput = shellcheckProcess.StandardOutput.ReadToEnd();
string shellcheckErrors = shellcheckProcess.StandardError.ReadToEnd();
shellcheckProcess.WaitForExit();
if (shellcheckProcess.ExitCode != 0)
{
Console.WriteLine($"ShellCheck found syntax errors: {shellcheckOutput}");
Console.WriteLine($"ShellCheck errors: {shellcheckErrors}");
Assert.Fail($"ShellCheck validation failed with exit code {shellcheckProcess.ExitCode}. Output: {shellcheckOutput}. Errors: {shellcheckErrors}");
}
else
{
Console.WriteLine("ShellCheck validation passed");
}
}
else
{
Console.WriteLine("ShellCheck not found, skipping additional validation");
}
}
private string ReplaceCommonPlaceholders(string template, string rootDirectory, string tempDir)
{
template = template.Replace("_PROCESS_ID_", "1234");
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener");
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
template = template.Replace("_SERVICEUSERNAME_", "runner");
template = template.Replace("_SERVICEPASSWORD_", "password");
template = template.Replace("_SERVICEDISPLAYNAME_", "GitHub Actions Runner");
template = template.Replace("_SERVICENAME_", "github-runner");
template = template.Replace("_SERVICELOGPATH_", Path.Combine(tempDir, "service.log"));
template = template.Replace("_RUNNERSERVICEUSERDISPLAYNAME_", "GitHub Actions Runner Service");
return template;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void UpdateShTemplateHasValidSyntax()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
}
catch (Exception ex)
{
Console.WriteLine($"Error during test: {ex}");
throw;
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void DarwinSvcShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "darwin.svc.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void DarwinSvcShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutbin",
"darwin.svc.sh.template",
shouldPass: false,
templateModifier: template =>
{
template = template.Replace("fi\n", "\n");
template = template.Replace("esac", "");
template = template.Replace("\"$svcuser\"", "\"$svcuser");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void SystemdSvcShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "systemd.svc.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void SystemdSvcShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutbin",
"systemd.svc.sh.template",
shouldPass: false,
templateModifier: template =>
{
template = template.Replace("done\n", "\n");
template = template.Replace("function", "function (");
template = template.Replace("if [ ! -f ", "if ! -f ");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void RunHelperShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutroot", "run-helper.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void RunHelperShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutroot",
"run-helper.sh.template",
shouldPass: false,
templateModifier: template =>
{
template = template.Replace("${RUNNER_ROOT}", "${RUNNER_ROOT");
template = template.Replace("\"$@\"", "\"$@");
template = template.Replace("> /dev/null", ">> >>");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void ValidateShellScript_MissingTemplate_ThrowsException()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "non_existent_template.sh.template", shouldPass: true);
Assert.Fail("Expected exception was not thrown");
}
catch (Exception ex)
{
Assert.Contains("non_existent_template.sh.template", ex.Message);
Assert.Contains("FileNotFoundException", ex.Message);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void ValidateShellScript_ComplexScript_ValidatesCorrectly()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
// Create a test template with complex shell scripting patterns
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string templatePath = Path.Combine(tempDir, "complex_shell.sh.template");
// Write a sample template with various shell features
string template = @"#!/bin/bash
set -e
# Function with nested quotes and complex syntax
function complex_func() {
local var1=""$1""
local var2=""${2:-default}""
echo ""Function arguments: '$var1' and '$var2'""
if [ ""$var1"" == ""test"" ]; then
echo ""This is a 'test' with nested quotes""
fi
}
# Complex variable substitutions
VAR1=""test value""
VAR2=""${VAR1:0:4}""
VAR3=""$(echo ""command substitution"")""
# Here document
cat << EOF > /tmp/testfile
This is a test file
With multiple lines
And some $VAR1 substitution
EOF
complex_func ""test"" ""value""
exit 0";
File.WriteAllText(templatePath, template);
try
{
ValidateShellScriptTemplateSyntax("", templatePath, shouldPass: true, useFullPath: true);
}
finally
{
// Clean up
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void UpdateCmdTemplateHasValidSyntax()
{
// Skip on non-Windows platforms
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: true);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void UpdateCmdTemplateWithErrorsFailsValidation()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
templateModifier: template =>
{
template = template.Replace("if exist", "if exist (");
template = template.Replace("echo", "echo \"Unclosed quote");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void ValidateCmdScript_MissingTemplate_ThrowsFileNotFoundException()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", "non_existent_template.cmd.template");
string content = File.ReadAllText(templatePath);
Assert.Fail($"Expected FileNotFoundException was not thrown for {templatePath}");
}
catch (FileNotFoundException)
{
// This is expected, so test passes
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void ValidateCmdScript_ComplexQuoting_ValidatesCorrectly()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string templatePath = Path.Combine(tempDir, "complex_quotes.cmd.template");
string template = @"@echo off
echo ""This has ""nested"" quotes""
echo ""This has an escaped quote: \""test\""""
echo Simple command
if ""quoted condition"" == ""quoted condition"" (
echo ""Inside if block with quotes""
)";
File.WriteAllText(templatePath, template);
try
{
ValidateCmdScriptTemplateSyntax(templatePath, shouldPass: true, useFullPath: true);
}
finally
{
// Clean up
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void ValidateCmdScript_ComplexParentheses_ValidatesCorrectly()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string templatePath = Path.Combine(tempDir, "complex_parens.cmd.template");
string template = @"@echo off
echo Text with (parentheses)
echo ""Text with (parentheses inside quotes)""
if exist file.txt (
if exist other.txt (
echo Nested if blocks
) else (
echo Nested else
)
) else (
echo Outer else
)";
File.WriteAllText(templatePath, template);
try
{
ValidateCmdScriptTemplateSyntax(templatePath, shouldPass: true, useFullPath: true);
}
finally
{
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
private bool HasUnclosedQuotes(string text)
{
bool inQuote = false;
bool isEscaped = false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (c == '\\')
{
isEscaped = !isEscaped;
continue;
}
if (c == '"' && !isEscaped)
{
inQuote = !inQuote;
}
if (c != '\\')
{
isEscaped = false;
}
}
return inQuote;
}
private bool HasBalancedParentheses(string text)
{
int balance = 0;
bool inQuote = false;
bool isEscaped = false;
bool inComment = false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (inComment)
{
if (c == '\n' || c == '\r')
{
inComment = false;
}
continue;
}
if (!inQuote && i < text.Length - 1 && c == ':' && text[i+1] == ':')
{
inComment = true;
continue;
}
if (!inQuote && i < text.Length - 2 && c == 'r' && text[i+1] == 'e' && text[i+2] == 'm' &&
(i == 0 || char.IsWhiteSpace(text[i-1])))
{
inComment = true;
continue;
}
if (c == '\\')
{
isEscaped = !isEscaped;
continue;
}
if (c == '"' && !isEscaped)
{
inQuote = !inQuote;
}
if (!inQuote)
{
if (c == '(')
{
balance++;
}
else if (c == ')')
{
balance--;
if (balance < 0)
{
return false;
}
}
}
if (c != '\\')
{
isEscaped = false;
}
}
return balance == 0;
}
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null, bool useFullPath = false)
{
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string templatePath;
if (useFullPath)
{
templatePath = templateName;
}
else
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string tempUpdatePath = Path.Combine(tempDir, Path.GetFileName(templatePath).Replace(".template", ""));
string template = File.ReadAllText(templatePath);
if (templateModifier != null)
{
template = templateModifier(template);
}
template = template.Replace("_PROCESS_ID_", "1234");
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener.exe");
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
template = template.Replace("_ROOT_FOLDER_", rootFolder);
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
File.WriteAllText(tempUpdatePath, template);
string errors = string.Empty;
string output = string.Empty;
int exitCode = 0;
try
{
string testBatchFile = Path.Combine(tempDir, "test.cmd");
File.WriteAllText(testBatchFile, "@echo off\r\nexit /b 0");
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c \"cd /d \"{tempDir}\" && echo Script syntax check && exit /b 0\"";
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = tempDir;
process.Start();
output = process.StandardOutput.ReadToEnd();
errors = process.StandardError.ReadToEnd();
process.WaitForExit();
exitCode = process.ExitCode;
}
catch (Exception ex)
{
errors = ex.ToString();
exitCode = 1;
}
bool hasMissingParenthesis = !HasBalancedParentheses(template);
bool hasUnclosedQuotes = HasUnclosedQuotes(template);
bool hasOutputErrors = !string.IsNullOrEmpty(errors) ||
output.Contains("syntax error") ||
output.Contains("not recognized") ||
output.Contains("unexpected") ||
output.Contains("Syntax check failed");
bool hasInvalidSyntaxPatterns = false;
if (template.Contains("if") && !template.Contains("if "))
{
hasInvalidSyntaxPatterns = true;
}
if (template.Contains("goto") && !template.Contains("goto "))
{
hasInvalidSyntaxPatterns = true;
}
if (template.Contains("(") && !template.Contains(")"))
{
hasInvalidSyntaxPatterns = true;
}
bool staticAnalysisPassed = !hasMissingParenthesis &&
!hasUnclosedQuotes &&
!hasInvalidSyntaxPatterns;
bool executionPassed = true;
try
{
if (!errors.Contains("filename, directory name, or volume label syntax"))
{
executionPassed = exitCode == 0 && !hasOutputErrors;
}
}
catch
{
executionPassed = true;
}
bool validationPassed = staticAnalysisPassed && executionPassed;
if (shouldPass)
{
Assert.True(validationPassed,
$"Template validation should have passed but failed. Exit code: {exitCode}, " +
$"Errors: {errors}, HasMissingParenthesis: {hasMissingParenthesis}, " +
$"HasUnclosedQuotes: {hasUnclosedQuotes}");
}
else
{
Assert.False(validationPassed,
"Template validation should have failed but passed. " +
"The intentionally introduced syntax errors were not detected.");
}
// Cleanup
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
catch (Exception ex)
{
Assert.Fail($"Exception during test: {ex.ToString()}");
}
}
}
}

View File

@@ -1659,6 +1659,76 @@ runs:
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")]

View File

@@ -505,6 +505,49 @@ namespace GitHub.Runner.Common.Tests.Worker
[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")]
public void Load_NodeAction_Pre()
{
try
@@ -758,7 +801,7 @@ namespace GitHub.Runner.Common.Tests.Worker
//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
{

View File

@@ -33,6 +33,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[InlineData("node12", "node20")]
[InlineData("node16", "node20")]
[InlineData("node20", "node20")]
[InlineData("node24", "node24")]
public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion)
{
using (TestHostContext hc = CreateTestContext())
@@ -41,7 +42,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var hf = new HandlerFactory();
hf.Initialize(hc);
// Server Feature Flag
// Setup variables
var variables = new Dictionary<string, VariableValue>();
Variables serverVariables = new(hc, variables);
@@ -72,5 +73,48 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal(expectedVersion, handler.Data.NodeVersion);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node24ExplicitlyRequested_HonoredByDefault()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
// Basic variables setup
var variables = new Dictionary<string, VariableValue>();
Variables serverVariables = new(hc, variables);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>()
});
// Act - Node 24 explicitly requested in action.yml
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
var handler = hf.Create(
_ec.Object,
new ScriptReference(),
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
// Assert - should be node24 as requested
Assert.Equal("node24", handler.Data.NodeVersion);
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -162,6 +162,60 @@ namespace GitHub.Runner.Common.Tests.Worker
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
}
}

View File

@@ -0,0 +1,63 @@
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers;
using Moq;
using System;
using System.Runtime.InteropServices;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class StepHostNodeVersionL0
{
private Mock<IExecutionContext> _ec;
private DefaultStepHost _defaultStepHost;
public StepHostNodeVersionL0()
{
_ec = new Mock<IExecutionContext>();
_defaultStepHost = new DefaultStepHost();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_Node24OnArm32Linux()
{
// Test via NodeUtil directly
string preferredVersion = "node24";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
// On ARM32 Linux, we should fall back to node20
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
// Should downgrade to node20 on ARM32 Linux
Assert.Equal("node20", nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains("Node 24 is not supported on Linux ARM32 platforms", warningMessage);
}
else
{
// On non-ARM32 platforms, should pass through the version unmodified
Assert.Equal("node24", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_PassThroughNonNode24Versions()
{
string preferredVersion = "node20";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
// Should never modify the version for non-node24 inputs
Assert.Equal("node20", nodeVersion);
Assert.Null(warningMessage);
}
}
}

View File

@@ -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'

View File

@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
PACKAGE_DIR="$SCRIPT_DIR/../_package"
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
DOTNETSDK_VERSION="8.0.411"
DOTNETSDK_VERSION="8.0.412"
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
RUNNER_VERSION=$(cat runnerversion)

View File

@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.411"
"version": "8.0.412"
}
}

View File

@@ -1 +1 @@
2.326.0
2.327.0