Compare commits

..

11 Commits

Author SHA1 Message Date
Salman Chishti
53a21f1a10 Merge branch 'salmanmkc/6-template-validation-syntax-tests' of https://github.com/actions/runner into salmanmkc/6-template-validation-syntax-tests 2025-07-27 21:15:00 +00:00
Salman Chishti
284c8ea43c add syntax error testing 2025-07-27 21:14:23 +00:00
Salman Chishti
1724385ca1 Update src/Test/L0/Listener/ShellScriptSyntaxL0.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-27 22:02:55 +01:00
Salman Chishti
0bc856255b Add tests for templates, since they are not validated 2025-07-27 21:01:02 +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
24 changed files with 842 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

@@ -1 +1 @@
2.326.0
<Update to ./src/runnerversion when creating release>

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,16 +136,22 @@ 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 node12
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
then
nodever="node12"
nodever="node16"
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 node12
then
nodever="node12"
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" ]]

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, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
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

@@ -50,8 +50,11 @@ namespace GitHub.Runner.Worker
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
!string.IsNullOrEmpty(orchestrationId.Value))
{
// 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));
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,388 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Sdk;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener
{
public sealed class ShellScriptSyntaxL0
{
// Generic method to test any shell script template for bash syntax errors
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null)
{
// Skip on Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string 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));
// Read the template
string template = File.ReadAllText(templatePath);
// Apply template modifier if provided (for injecting errors)
if (templateModifier != null)
{
template = templateModifier(template);
}
// Replace common placeholders with valid test values
template = ReplaceCommonPlaceholders(template, rootDirectory, tempDir);
// Write the processed template to a temporary file
File.WriteAllText(tempScriptPath, template);
// Make the file executable
var chmodProcess = new Process();
chmodProcess.StartInfo.FileName = "chmod";
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
chmodProcess.Start();
chmodProcess.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();
// Assert based on expected outcome
if (shouldPass)
{
Assert.Equal(0, process.ExitCode);
Assert.Empty(errors);
}
else
{
Assert.NotEqual(0, process.ExitCode);
Assert.NotEmpty(errors);
}
// Cleanup
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
catch (Exception ex)
{
Assert.Fail($"Exception during test for {templateName}: {ex}");
}
}
// Helper method to replace common placeholders in shell script templates
private string ReplaceCommonPlaceholders(string template, string rootDirectory, string tempDir)
{
// Replace common placeholders
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()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void UpdateShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutbin",
"update.sh.template",
shouldPass: false,
templateModifier: template =>
{
// Introduce syntax errors
// 1. Missing 'fi' for an 'if' statement
template = template.Replace("fi\n", "\n");
// 2. Unbalanced quotes
template = template.Replace("date \"+[%F %T-%4N]", "date \"+[%F %T-%4N");
// 3. Invalid syntax in if condition
template = template.Replace("if [ $? -ne 0 ]", "if [ $? -ne 0");
return template;
});
}
[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 SystemdSvcShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "systemd.svc.sh.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 UpdateShTemplateHasCorrectVariableReferencesAndIfStructure()
{
// Skip on Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", "update.sh.template");
// Read the template
string template = File.ReadAllText(templatePath);
// Assert
// 1. Check that $restartinteractiverunner is correctly referenced with $ in if condition
Assert.Contains("if [[ \"$currentplatform\" == 'darwin' && $restartinteractiverunner -eq 0 ]];\nthen", template);
// 2. Check for proper nesting of if statements for node version checks
int nodeVersionCheckLines = 0;
bool foundNode24Block = false;
bool foundNode16Block = false;
bool foundNode12Block = false;
bool hasProperIndentation = false;
string[] lines = template.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
if (line.Contains("nodever=\"node24\""))
{
foundNode24Block = true;
}
if (line.Contains("nodever=\"node16\""))
{
foundNode16Block = true;
}
if (foundNode16Block && line.Contains("nodever=\"node12\""))
{
foundNode12Block = true;
// Check if we have proper indentation for this nested block
hasProperIndentation = line.StartsWith(" ");
}
if (line.Contains("Fallback if RunnerService.js was started with"))
{
nodeVersionCheckLines++;
}
}
// The template has node24 check but there's no "Fallback if RunnerService.js was started with node24" comment for it
// Only the node20, node16, and node12 sections have this comment
Assert.Equal(3, nodeVersionCheckLines); // node20, node16, node12
Assert.True(foundNode24Block, "Could not find node24 block");
Assert.True(foundNode16Block, "Could not find node16 block");
Assert.True(foundNode12Block, "Could not find node12 block");
Assert.True(hasProperIndentation, "node12 block is not properly indented");
}
}
catch (Exception ex)
{
Assert.Fail($"Exception during test: {ex.ToString()}");
}
}
[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()
{
// Skip on non-Windows platforms
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
templateModifier: template =>
{
// Introduce syntax errors in the template
// 1. Unbalanced parentheses
template = template.Replace("if exist", "if exist (");
// 2. Unclosed quotes
template = template.Replace("echo", "echo \"Unclosed quote");
return template;
});
}
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null)
{
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string 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(templateName).Replace(".template", ""));
// Read the template
string template = File.ReadAllText(templatePath);
// Apply template modifier if provided (for injecting errors)
if (templateModifier != null)
{
template = templateModifier(template);
}
// Replace the placeholders with valid test values
template = template.Replace("_PROCESS_ID_", "1234");
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener.exe");
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");
// Write the processed template to a temporary file
File.WriteAllText(tempUpdatePath, template);
// Act - Check syntax using cmd with special flags:
// /v:on - Enable delayed environment variable expansion
// /f:off - Disable file name completion
// /e:on - Enable command extensions
// These flags help validate the syntax without fully executing the script
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c /v:on /f:off /e:on \"{tempUpdatePath}\" echo SyntaxCheckOnly && exit /b 0";
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
string errors = process.StandardError.ReadToEnd();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
// Check for mismatched parentheses in the file content
int openParenCount = template.Split('(').Length - 1;
int closeParenCount = template.Split(')').Length - 1;
bool hasMissingParenthesis = openParenCount != closeParenCount;
// Check for unclosed quotes (simple check - not perfect but catches obvious errors)
int doubleQuoteCount = template.Split('"').Length - 1;
bool hasUnclosedQuotes = doubleQuoteCount % 2 != 0;
// Determine if the validation passed
bool validationPassed = process.ExitCode == 0 &&
string.IsNullOrEmpty(errors) &&
!hasMissingParenthesis &&
!hasUnclosedQuotes;
// Assert based on expected outcome
if (shouldPass)
{
Assert.True(validationPassed,
$"Template validation should have passed but failed. Exit code: {process.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

@@ -502,6 +502,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")]
@@ -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