Skip to content

Commit 2fe31d9

Browse files
committed
Add Bun runtime support for JavaScript actions
Signed-off-by: Vladislav Polyakov <[email protected]>
1 parent 54bcc00 commit 2fe31d9

File tree

13 files changed

+616
-23
lines changed

13 files changed

+616
-23
lines changed

src/Misc/externals.sh

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
99
NODE20_VERSION="20.19.5"
1010
NODE24_VERSION="24.11.1"
1111

12+
BUN_URL=https://github.com/oven-sh/bun/releases/download
13+
BUN_VERSION="1.3.2"
14+
1215
get_abs_path() {
1316
# exploits the fact that pwd will print abs path when no args
1417
echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
@@ -142,18 +145,26 @@ if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
142145
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
143146
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
144147
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
148+
149+
# Note: Bun is only available for Windows x64, not for win-x86 (32-bit Windows)
150+
if [[ "$PACKAGERUNTIME" == "win-x64" ]]; then
151+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-windows-x64.zip" bun/bin fix_nested_dir
152+
fi
153+
145154
if [[ "$PRECACHE" != "" ]]; then
146155
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
147156
fi
148157
fi
149158

150-
# Download the external tools only for Windows.
159+
# Download the external tools only for Windows ARM64.
160+
# Note: Bun doesn't have official Windows ARM64 release yet, so we skip it for now.
151161
if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
152162
# todo: replace these with official release when available
153163
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
154164
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
155165
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
156166
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
167+
157168
if [[ "$PRECACHE" != "" ]]; then
158169
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
159170
fi
@@ -163,12 +174,14 @@ fi
163174
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
164175
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir
165176
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-x64.tar.gz" node24 fix_nested_dir
177+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-darwin-x64.zip" bun/bin fix_nested_dir
166178
fi
167179

168180
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
169181
# node.js v12 doesn't support macOS on arm64.
170182
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-arm64.tar.gz" node20 fix_nested_dir
171183
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-arm64.tar.gz" node24 fix_nested_dir
184+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-darwin-aarch64.zip" bun/bin fix_nested_dir
172185
fi
173186

174187
# Download the external tools for Linux PACKAGERUNTIMEs.
@@ -177,11 +190,15 @@ if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
177190
acquireExternalTool "$NODE_ALPINE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine
178191
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-x64.tar.gz" node24 fix_nested_dir
179192
acquireExternalTool "$NODE_ALPINE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-alpine-x64.tar.gz" node24_alpine
193+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-x64.zip" bun/bin fix_nested_dir
194+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-x64-musl.zip" bun_alpine/bin fix_nested_dir
180195
fi
181196

182197
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
183198
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-arm64.tar.gz" node20 fix_nested_dir
184199
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-arm64.tar.gz" node24 fix_nested_dir
200+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-aarch64.zip" bun/bin fix_nested_dir
201+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-aarch64-musl.zip" bun_alpine/bin fix_nested_dir
185202
fi
186203

187204
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then

src/Runner.Common/Constants.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,19 +173,20 @@ public static class Features
173173
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check";
174174
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
175175
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
176+
public static readonly string AllowBunRuntime = "actions.runner.allowbunruntime";
176177
}
177-
178+
178179
// Node version migration related constants
179180
public static class NodeMigration
180181
{
181182
// Node versions
182183
public static readonly string Node20 = "node20";
183184
public static readonly string Node24 = "node24";
184-
185+
185186
// Environment variables for controlling node version selection
186187
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
187188
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
188-
189+
189190
// Feature flags for controlling the migration phases
190191
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
191192
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";

src/Runner.Worker/ActionManifestManager.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public ActionDefinitionDataNew Load(IExecutionContext executionContext, string m
5656
ActionDefinitionDataNew actionDefinition = new();
5757

5858
// Clean up file name real quick
59-
// Instead of using Regex which can be computationally expensive,
59+
// Instead of using Regex which can be computationally expensive,
6060
// we can just remove the # of characters from the fileName according to the length of the basePath
6161
string basePath = HostContext.GetDirectory(WellKnownDirectory.Actions);
6262
string fileRelativePath = manifestFile;
@@ -464,7 +464,8 @@ private ActionExecutionData ConvertRuns(
464464
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
465465
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
466466
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
467-
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
467+
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase) ||
468+
string.Equals(usingToken.Value, "bun", StringComparison.OrdinalIgnoreCase))
468469
{
469470
if (string.IsNullOrEmpty(mainToken?.Value))
470471
{
@@ -504,7 +505,7 @@ private ActionExecutionData ConvertRuns(
504505
}
505506
else
506507
{
507-
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
508+
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun' instead.");
508509
}
509510
}
510511
else if (pluginToken != null)
@@ -515,7 +516,7 @@ private ActionExecutionData ConvertRuns(
515516
};
516517
}
517518

518-
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
519+
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun'.");
519520
}
520521

521522
private void ConvertInputs(
@@ -600,4 +601,3 @@ public sealed class CompositeActionExecutionDataNew : ActionExecutionData
600601
public MappingToken Outputs { get; set; }
601602
}
602603
}
603-

src/Runner.Worker/ActionManifestManagerLegacy.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public ActionDefinitionData Load(IExecutionContext executionContext, string mani
5656
ActionDefinitionData actionDefinition = new();
5757

5858
// Clean up file name real quick
59-
// Instead of using Regex which can be computationally expensive,
59+
// Instead of using Regex which can be computationally expensive,
6060
// we can just remove the # of characters from the fileName according to the length of the basePath
6161
string basePath = HostContext.GetDirectory(WellKnownDirectory.Actions);
6262
string fileRelativePath = manifestFile;
@@ -451,7 +451,8 @@ private ActionExecutionData ConvertRuns(
451451
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
452452
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
453453
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
454-
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
454+
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase) ||
455+
string.Equals(usingToken.Value, "bun", StringComparison.OrdinalIgnoreCase))
455456
{
456457
if (string.IsNullOrEmpty(mainToken?.Value))
457458
{
@@ -491,7 +492,7 @@ private ActionExecutionData ConvertRuns(
491492
}
492493
else
493494
{
494-
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
495+
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun' instead.");
495496
}
496497
}
497498
else if (pluginToken != null)
@@ -502,7 +503,7 @@ private ActionExecutionData ConvertRuns(
502503
};
503504
}
504505

505-
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
506+
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun'.");
506507
}
507508

508509
private void ConvertInputs(
@@ -543,4 +544,3 @@ private void ConvertInputs(
543544
}
544545
}
545546
}
546-

src/Runner.Worker/FeatureManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@ public static bool IsContainerActionRunnerTempEnabled(Variables variables)
1616
{
1717
return variables?.GetBoolean(Constants.Runner.Features.ContainerActionRunnerTemp) ?? false;
1818
}
19+
20+
public static bool IsBunRuntimeEnabled(Variables variables)
21+
{
22+
return variables?.GetBoolean(Constants.Runner.Features.AllowBunRuntime) ?? false;
23+
}
1924
}
2025
}

src/Runner.Worker/Handlers/NodeScriptActionHandler.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,28 @@ public async Task RunAsync(ActionRunStage stage)
110110
workingDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
111111
}
112112

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

117-
// Format the arguments passed to node.
116+
if (isBun)
117+
{
118+
if (!FeatureManager.IsBunRuntimeEnabled(ExecutionContext.Global.Variables))
119+
{
120+
throw new NotSupportedException($"Bun runtime is not enabled. Please enable the feature flag '{Constants.Runner.Features.AllowBunRuntime}' to use Bun runtime.");
121+
}
122+
123+
var bunRuntimeVersion = await StepHost.DetermineBunRuntimeVersion(ExecutionContext);
124+
ExecutionContext.StepTelemetry.Type = bunRuntimeVersion;
125+
file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), bunRuntimeVersion, "bin", $"bun{IOUtil.ExeExtension}");
126+
}
127+
else
128+
{
129+
var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion);
130+
ExecutionContext.StepTelemetry.Type = nodeRuntimeVersion;
131+
file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
132+
}
133+
134+
// Format the arguments passed to node/bun.
118135
// 1) Wrap the script file path in double quotes.
119136
// 2) Escape double quotes within the script file path. Double-quote is a valid
120137
// file name character on Linux.
@@ -128,7 +145,8 @@ public async Task RunAsync(ActionRunStage stage)
128145
Encoding outputEncoding = null;
129146
#endif
130147

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

134152
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
@@ -162,7 +180,8 @@ public async Task RunAsync(ActionRunStage stage)
162180
else
163181
{
164182
var exitCode = await step;
165-
ExecutionContext.Debug($"Node Action run completed with exit code {exitCode}");
183+
string runtimeName = isBun ? "Bun" : "Node";
184+
ExecutionContext.Debug($"{runtimeName} Action run completed with exit code {exitCode}");
166185
if (exitCode != 0)
167186
{
168187
ExecutionContext.Result = TaskResult.Failed;

src/Runner.Worker/Handlers/StepHost.cs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public interface IStepHost : IRunnerService
2121

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

24+
Task<string> DetermineBunRuntimeVersion(IExecutionContext executionContext);
25+
2426
Task<int> ExecuteAsync(IExecutionContext context,
2527
string workingDirectory,
2628
string fileName,
@@ -64,10 +66,31 @@ public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionConte
6466
{
6567
executionContext.Warning(warningMessage);
6668
}
67-
69+
6870
return Task.FromResult(nodeVersion);
6971
}
7072

73+
public Task<string> DetermineBunRuntimeVersion(IExecutionContext executionContext)
74+
{
75+
// Check platform compatibility for Bun runtime
76+
if (Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X86))
77+
{
78+
var os = Constants.Runner.Platform.ToString();
79+
var msg = $"Bun runtime is not supported on {os} x86 (32-bit) platforms.";
80+
throw new NotSupportedException(msg);
81+
}
82+
83+
if (Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
84+
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
85+
{
86+
var msg = "Bun runtime is not supported on Linux ARM32 platforms.";
87+
throw new NotSupportedException(msg);
88+
}
89+
90+
// Bun runtime version is simply "bun"
91+
return Task.FromResult("bun");
92+
}
93+
7194
public async Task<int> ExecuteAsync(IExecutionContext context,
7295
string workingDirectory,
7396
string fileName,
@@ -183,6 +206,59 @@ public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executio
183206
return nodeExternal;
184207
}
185208

209+
public async Task<string> DetermineBunRuntimeVersion(IExecutionContext executionContext)
210+
{
211+
string bunExternal = "bun";
212+
213+
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
214+
{
215+
if (Container.IsAlpine)
216+
{
217+
bunExternal = CheckPlatformForAlpineBunContainer(executionContext);
218+
}
219+
executionContext.Debug($"Running JavaScript Action with Bun runtime: {bunExternal}");
220+
return bunExternal;
221+
}
222+
223+
// Best effort to determine a compatible bun runtime
224+
// Check if we're in an Alpine container
225+
var osReleaseIdCmd = "sh -c \"cat /etc/*release | grep ^ID\"";
226+
var dockerManager = HostContext.GetService<IDockerCommandManager>();
227+
228+
var output = new List<string>();
229+
var execExitCode = await dockerManager.DockerExec(executionContext, Container.ContainerId, string.Empty, osReleaseIdCmd, output);
230+
if (execExitCode == 0)
231+
{
232+
foreach (var line in output)
233+
{
234+
executionContext.Debug(line);
235+
if (line.ToLower().Contains("alpine"))
236+
{
237+
bunExternal = CheckPlatformForAlpineBunContainer(executionContext);
238+
return bunExternal;
239+
}
240+
}
241+
}
242+
executionContext.Debug($"Running JavaScript Action with Bun runtime: {bunExternal}");
243+
return bunExternal;
244+
}
245+
246+
private string CheckPlatformForAlpineBunContainer(IExecutionContext executionContext)
247+
{
248+
// Check for Alpine container compatibility
249+
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64) &&
250+
!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm64))
251+
{
252+
var os = Constants.Runner.Platform.ToString();
253+
var arch = Constants.Runner.PlatformArchitecture.ToString();
254+
var msg = $"Bun Actions in Alpine containers are only supported on x64 and ARM64 Linux runners. Detected {os} {arch}";
255+
throw new NotSupportedException(msg);
256+
}
257+
string bunExternal = "bun_alpine";
258+
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with Bun runtime: {bunExternal}");
259+
return bunExternal;
260+
}
261+
186262
public async Task<int> ExecuteAsync(IExecutionContext context,
187263
string workingDirectory,
188264
string fileName,

0 commit comments

Comments
 (0)