Skip to content

Commit 5d36125

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

File tree

7 files changed

+110
-20
lines changed

7 files changed

+110
-20
lines changed

src/Misc/externals.sh

Lines changed: 13 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,21 @@ 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+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-windows-x64.zip" bun/bin fix_nested_dir
145149
if [[ "$PRECACHE" != "" ]]; then
146150
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
147151
fi
148152
fi
149153

150-
# Download the external tools only for Windows.
154+
# Download the external tools only for Windows ARM64.
155+
# Note: Bun doesn't have official Windows ARM64 release yet, so we skip it for now
151156
if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
152157
# todo: replace these with official release when available
153158
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
154159
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
155160
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
156161
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
162+
# Bun Windows ARM64 not available yet
157163
if [[ "$PRECACHE" != "" ]]; then
158164
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
159165
fi
@@ -163,12 +169,14 @@ fi
163169
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
164170
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir
165171
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-x64.tar.gz" node24 fix_nested_dir
172+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-darwin-x64.zip" bun/bin fix_nested_dir
166173
fi
167174

168175
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
169176
# node.js v12 doesn't support macOS on arm64.
170177
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-arm64.tar.gz" node20 fix_nested_dir
171178
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-arm64.tar.gz" node24 fix_nested_dir
179+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-darwin-aarch64.zip" bun/bin fix_nested_dir
172180
fi
173181

174182
# Download the external tools for Linux PACKAGERUNTIMEs.
@@ -177,11 +185,15 @@ if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
177185
acquireExternalTool "$NODE_ALPINE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine
178186
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-x64.tar.gz" node24 fix_nested_dir
179187
acquireExternalTool "$NODE_ALPINE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-alpine-x64.tar.gz" node24_alpine
188+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-x64.zip" bun/bin fix_nested_dir
189+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-x64-musl.zip" bun_alpine/bin fix_nested_dir
180190
fi
181191

182192
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
183193
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-arm64.tar.gz" node20 fix_nested_dir
184194
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-arm64.tar.gz" node24 fix_nested_dir
195+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-aarch64.zip" bun/bin fix_nested_dir
196+
acquireExternalTool "$BUN_URL/bun-v${BUN_VERSION}/bun-linux-aarch64-musl.zip" bun_alpine/bin fix_nested_dir
185197
fi
186198

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

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/Handlers/NodeScriptActionHandler.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,23 @@ 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+
var bunRuntimeVersion = await StepHost.DetermineBunRuntimeVersion(ExecutionContext);
119+
ExecutionContext.StepTelemetry.Type = bunRuntimeVersion;
120+
file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), bunRuntimeVersion, "bin", $"bun{IOUtil.ExeExtension}");
121+
}
122+
else
123+
{
124+
var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion);
125+
ExecutionContext.StepTelemetry.Type = nodeRuntimeVersion;
126+
file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
127+
}
128+
129+
// Format the arguments passed to node/bun.
118130
// 1) Wrap the script file path in double quotes.
119131
// 2) Escape double quotes within the script file path. Double-quote is a valid
120132
// file name character on Linux.
@@ -129,7 +141,11 @@ public async Task RunAsync(ActionRunStage stage)
129141
#endif
130142

131143
// Remove environment variable that may cause conflicts with the node within the runner.
132-
Environment.Remove("NODE_ICU_DATA"); // https://github.com/actions/runner/issues/795
144+
// Only remove for Node.js, not for Bun
145+
if (!isBun)
146+
{
147+
Environment.Remove("NODE_ICU_DATA"); // https://github.com/actions/runner/issues/795
148+
}
133149

134150
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
135151
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
@@ -162,7 +178,8 @@ public async Task RunAsync(ActionRunStage stage)
162178
else
163179
{
164180
var exitCode = await step;
165-
ExecutionContext.Debug($"Node Action run completed with exit code {exitCode}");
181+
string runtimeName = isBun ? "Bun" : "Node";
182+
ExecutionContext.Debug($"{runtimeName} Action run completed with exit code {exitCode}");
166183
if (exitCode != 0)
167184
{
168185
ExecutionContext.Result = TaskResult.Failed;

src/Runner.Worker/Handlers/StepHost.cs

Lines changed: 62 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,16 @@ 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+
// Bun runtime version is simply "bun"
76+
return Task.FromResult("bun");
77+
}
78+
7179
public async Task<int> ExecuteAsync(IExecutionContext context,
7280
string workingDirectory,
7381
string fileName,
@@ -183,6 +191,59 @@ public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executio
183191
return nodeExternal;
184192
}
185193

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

src/Test/L0/Worker/ActionManifestManagerL0.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ public void Load_CompositeActionNoUsing()
803803
//Assert
804804
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
805805
Assert.Contains($"Failed to load {action_path}", err.Message);
806-
_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);
806+
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
807807
}
808808
finally
809809
{

src/Test/L0/Worker/ActionManifestManagerLegacyL0.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ public void Load_CompositeActionNoUsing()
801801
//Assert
802802
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
803803
Assert.Contains($"Failed to load {action_path}", err.Message);
804-
_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);
804+
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20', 'node24' or 'bun'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
805805
}
806806
finally
807807
{

0 commit comments

Comments
 (0)