Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/Misc/externals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
NODE20_VERSION="20.19.5"
NODE24_VERSION="24.11.1"

BUN_URL=https://github.com/oven-sh/bun/releases/download
BUN_VERSION="1.3.2"

get_abs_path() {
# exploits the fact that pwd will print abs path when no args
echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
Expand Down Expand Up @@ -142,18 +145,26 @@ if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
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

# Note: Bun is only available for Windows x64, not for win-x86 (32-bit Windows)
if [[ "$PACKAGERUNTIME" == "win-x64" ]]; then
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-windows-x64.zip" bun/bin fix_nested_dir
fi

if [[ "$PRECACHE" != "" ]]; then
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
fi
fi

# Download the external tools only for Windows.
# Download the external tools only for Windows ARM64.
# Note: Bun doesn't have official Windows ARM64 release yet, so we skip it for now.
if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
# todo: replace these with official release when available
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin

if [[ "$PRECACHE" != "" ]]; then
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
fi
Expand All @@ -163,12 +174,14 @@ fi
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
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-darwin-x64.zip" bun/bin 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
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-darwin-aarch64.zip" bun/bin fix_nested_dir
fi

# Download the external tools for Linux PACKAGERUNTIMEs.
Expand All @@ -177,11 +190,15 @@ if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
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
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-x64.zip" bun/bin fix_nested_dir
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-x64-musl.zip" bun_alpine/bin fix_nested_dir
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
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-aarch64.zip" bun/bin fix_nested_dir
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-aarch64-musl.zip" bun_alpine/bin fix_nested_dir
fi

if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then
Expand Down
7 changes: 4 additions & 3 deletions src/Runner.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,20 @@ public static class Features
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check";
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
public static readonly string AllowBunRuntime = "actions.runner.allowbunruntime";
}

// Node version migration related constants
public static class NodeMigration
{
// Node versions
public static readonly string Node20 = "node20";
public static readonly string Node24 = "node24";

// Environment variables for controlling node version selection
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";

// Feature flags for controlling the migration phases
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
Expand Down
10 changes: 5 additions & 5 deletions src/Runner.Worker/ActionManifestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public ActionDefinitionDataNew Load(IExecutionContext executionContext, string m
ActionDefinitionDataNew actionDefinition = new();

// Clean up file name real quick
// Instead of using Regex which can be computationally expensive,
// Instead of using Regex which can be computationally expensive,
// we can just remove the # of characters from the fileName according to the length of the basePath
string basePath = HostContext.GetDirectory(WellKnownDirectory.Actions);
string fileRelativePath = manifestFile;
Expand Down Expand Up @@ -464,7 +464,8 @@ private ActionExecutionData ConvertRuns(
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "bun", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(mainToken?.Value))
{
Expand Down Expand Up @@ -504,7 +505,7 @@ private ActionExecutionData ConvertRuns(
}
else
{
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun' instead.");
}
}
else if (pluginToken != null)
Expand All @@ -515,7 +516,7 @@ private ActionExecutionData ConvertRuns(
};
}

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

private void ConvertInputs(
Expand Down Expand Up @@ -600,4 +601,3 @@ public sealed class CompositeActionExecutionDataNew : ActionExecutionData
public MappingToken Outputs { get; set; }
}
}

10 changes: 5 additions & 5 deletions src/Runner.Worker/ActionManifestManagerLegacy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public ActionDefinitionData Load(IExecutionContext executionContext, string mani
ActionDefinitionData actionDefinition = new();

// Clean up file name real quick
// Instead of using Regex which can be computationally expensive,
// Instead of using Regex which can be computationally expensive,
// we can just remove the # of characters from the fileName according to the length of the basePath
string basePath = HostContext.GetDirectory(WellKnownDirectory.Actions);
string fileRelativePath = manifestFile;
Expand Down Expand Up @@ -451,7 +451,8 @@ private ActionExecutionData ConvertRuns(
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "bun", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(mainToken?.Value))
{
Expand Down Expand Up @@ -491,7 +492,7 @@ private ActionExecutionData ConvertRuns(
}
else
{
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun' instead.");
}
}
else if (pluginToken != null)
Expand All @@ -502,7 +503,7 @@ private ActionExecutionData ConvertRuns(
};
}

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

private void ConvertInputs(
Expand Down Expand Up @@ -543,4 +544,3 @@ private void ConvertInputs(
}
}
}

5 changes: 5 additions & 0 deletions src/Runner.Worker/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ public static bool IsContainerActionRunnerTempEnabled(Variables variables)
{
return variables?.GetBoolean(Constants.Runner.Features.ContainerActionRunnerTemp) ?? false;
}

public static bool IsBunRuntimeEnabled(Variables variables)
{
return variables?.GetBoolean(Constants.Runner.Features.AllowBunRuntime) ?? false;
}
}
}
31 changes: 25 additions & 6 deletions src/Runner.Worker/Handlers/NodeScriptActionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,28 @@ public async Task RunAsync(ActionRunStage stage)
workingDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
}

var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion);
ExecutionContext.StepTelemetry.Type = nodeRuntimeVersion;
string file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
string file;
bool isBun = string.Equals(Data.NodeVersion, "bun", StringComparison.OrdinalIgnoreCase);

// Format the arguments passed to node.
if (isBun)
{
if (!FeatureManager.IsBunRuntimeEnabled(ExecutionContext.Global.Variables))
{
throw new NotSupportedException($"Bun runtime is not enabled. Please enable the feature flag '{Constants.Runner.Features.AllowBunRuntime}' to use Bun runtime.");
}

var bunRuntimeVersion = await StepHost.DetermineBunRuntimeVersion(ExecutionContext);
ExecutionContext.StepTelemetry.Type = bunRuntimeVersion;
file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), bunRuntimeVersion, "bin", $"bun{IOUtil.ExeExtension}");
}
else
{
var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion);
ExecutionContext.StepTelemetry.Type = nodeRuntimeVersion;
file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
}

// Format the arguments passed to node/bun.
// 1) Wrap the script file path in double quotes.
// 2) Escape double quotes within the script file path. Double-quote is a valid
// file name character on Linux.
Expand All @@ -128,7 +145,8 @@ public async Task RunAsync(ActionRunStage stage)
Encoding outputEncoding = null;
#endif

// Remove environment variable that may cause conflicts with the node within the runner.
// Remove environment variable that may cause conflicts with the node/bun within the runner.
// This applies to both Node.js and Bun to avoid conflicts with the runner's environment.
Environment.Remove("NODE_ICU_DATA"); // https://github.com/actions/runner/issues/795

using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
Expand Down Expand Up @@ -162,7 +180,8 @@ public async Task RunAsync(ActionRunStage stage)
else
{
var exitCode = await step;
ExecutionContext.Debug($"Node Action run completed with exit code {exitCode}");
string runtimeName = isBun ? "Bun" : "Node";
ExecutionContext.Debug($"{runtimeName} Action run completed with exit code {exitCode}");
if (exitCode != 0)
{
ExecutionContext.Result = TaskResult.Failed;
Expand Down
78 changes: 77 additions & 1 deletion src/Runner.Worker/Handlers/StepHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public interface IStepHost : IRunnerService

Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion);

Task<string> DetermineBunRuntimeVersion(IExecutionContext executionContext);

Task<int> ExecuteAsync(IExecutionContext context,
string workingDirectory,
string fileName,
Expand Down Expand Up @@ -64,10 +66,31 @@ public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionConte
{
executionContext.Warning(warningMessage);
}

return Task.FromResult(nodeVersion);
}

public Task<string> DetermineBunRuntimeVersion(IExecutionContext executionContext)
{
// Check platform compatibility for Bun runtime
if (Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X86))
{
var os = Constants.Runner.Platform.ToString();
var msg = $"Bun runtime is not supported on {os} x86 (32-bit) platforms.";
throw new NotSupportedException(msg);
}

if (Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
{
var msg = "Bun runtime is not supported on Linux ARM32 platforms.";
throw new NotSupportedException(msg);
}

// Bun runtime version is simply "bun"
return Task.FromResult("bun");
}

public async Task<int> ExecuteAsync(IExecutionContext context,
string workingDirectory,
string fileName,
Expand Down Expand Up @@ -183,6 +206,59 @@ public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executio
return nodeExternal;
}

public async Task<string> DetermineBunRuntimeVersion(IExecutionContext executionContext)
{
string bunExternal = "bun";

if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
if (Container.IsAlpine)
{
bunExternal = CheckPlatformForAlpineBunContainer(executionContext);
}
executionContext.Debug($"Running JavaScript Action with Bun runtime: {bunExternal}");
return bunExternal;
}

// Best effort to determine a compatible bun runtime
// Check if we're in an Alpine container
var osReleaseIdCmd = "sh -c \"cat /etc/*release | grep ^ID\"";
var dockerManager = HostContext.GetService<IDockerCommandManager>();

var output = new List<string>();
var execExitCode = await dockerManager.DockerExec(executionContext, Container.ContainerId, string.Empty, osReleaseIdCmd, output);
if (execExitCode == 0)
{
foreach (var line in output)
{
executionContext.Debug(line);
if (line.ToLower().Contains("alpine"))
{
bunExternal = CheckPlatformForAlpineBunContainer(executionContext);
return bunExternal;
}
}
}
executionContext.Debug($"Running JavaScript Action with Bun runtime: {bunExternal}");
return bunExternal;
}

private string CheckPlatformForAlpineBunContainer(IExecutionContext executionContext)
{
// Check for Alpine container compatibility
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64) &&
!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm64))
{
var os = Constants.Runner.Platform.ToString();
var arch = Constants.Runner.PlatformArchitecture.ToString();
var msg = $"Bun Actions in Alpine containers are only supported on x64 and ARM64 Linux runners. Detected {os} {arch}";
throw new NotSupportedException(msg);
}
string bunExternal = "bun_alpine";
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with Bun runtime: {bunExternal}");
return bunExternal;
}

public async Task<int> ExecuteAsync(IExecutionContext context,
string workingDirectory,
string fileName,
Expand Down
Loading