Skip to content

Commit 239205a

Browse files
committed
Add file-based script execution in ScriptRunner
1 parent 15af708 commit 239205a

File tree

2 files changed

+136
-5
lines changed

2 files changed

+136
-5
lines changed

lib/script_runner.ex

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,27 @@ defmodule ElixirScript.ScriptRunner do
2525

2626
Logger.debug("Created GitHub client: #{inspect(client)}")
2727

28+
bindings = [
29+
context: Context.from_github_environment(),
30+
client: client
31+
]
32+
2833
{value, _binding} =
29-
Code.eval_string(
30-
script,
31-
context: Context.from_github_environment(),
32-
client: client
33-
)
34+
if is_file_path?(script) do
35+
path = Path.expand(script)
36+
content = File.read!(path)
37+
Code.eval_string(content, bindings, file: path, line: 1)
38+
else
39+
Code.eval_string(script, bindings)
40+
end
3441

3542
value
3643
end
3744

45+
# Determines if the given string is a file path
46+
defp is_file_path?(script) when is_binary(script) do
47+
String.starts_with?(script, ["./", "../", "/"])
48+
end
49+
3850
defp tentacat_client, do: Application.get_env(:script_runner, :tentacat_client, Tentacat.Client)
3951
end

test/script_runner_test.exs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ defmodule ElixirScript.ScriptRunnerTest do
66
alias ElixirScript.ScriptRunner
77
alias Test.Fixtures.GitHubWorkflowRun
88

9+
# Create a temporary file with automatic cleanup
10+
# Returns the full path to the created file
11+
defp create_temp_file(filename, content) do
12+
path = Path.join(System.tmp_dir(), filename)
13+
File.write!(path, content)
14+
on_exit(fn -> File.rm!(path) end)
15+
path
16+
end
17+
918
setup do
1019
stub(SystemEnvMock, :get_env, fn varname, default ->
1120
GitHubWorkflowRun.env()[varname] || default
@@ -34,6 +43,116 @@ defmodule ElixirScript.ScriptRunnerTest do
3443
end
3544
end
3645

46+
describe "file-based scripts" do
47+
test "can execute scripts from file paths" do
48+
file_path = create_temp_file("test_script.exs", "\"Hello from test file\"")
49+
assert ScriptRunner.run(file_path) == "Hello from test file"
50+
end
51+
52+
test "file scripts have access to context bindings" do
53+
file_path = create_temp_file("context_test.exs", ~S["Actor is: #{context.actor}"])
54+
assert ScriptRunner.run(file_path) == "Actor is: gaggle"
55+
end
56+
57+
test "file scripts have access to client bindings" do
58+
file_path = create_temp_file("client_test.exs", "inspect(client)")
59+
result = ScriptRunner.run(file_path)
60+
assert result =~ "auth: nil"
61+
end
62+
63+
test "auto-detects file paths vs inline scripts" do
64+
# Test detection by behavior - file paths cause File.Error
65+
assert_raise File.Error, fn ->
66+
ScriptRunner.run("./non_existent.exs")
67+
end
68+
69+
assert_raise File.Error, fn ->
70+
ScriptRunner.run("../non_existent.exs")
71+
end
72+
73+
assert_raise File.Error, fn ->
74+
ScriptRunner.run("/non_existent.exs")
75+
end
76+
77+
# Inline scripts should be evaluated as code
78+
assert ScriptRunner.run("1 + 1") == 2
79+
assert ScriptRunner.run("\"hello\"") == "hello"
80+
end
81+
82+
test "raises clear error when file doesn't exist" do
83+
assert_raise File.Error, ~r/no such file or directory/, fn ->
84+
ScriptRunner.run("./non_existent_file.exs")
85+
end
86+
end
87+
88+
test "handles empty file gracefully" do
89+
file_path = create_temp_file("empty.exs", "")
90+
# Elixir naturally returns nil for empty files
91+
assert ScriptRunner.run(file_path) == nil
92+
end
93+
94+
test "handles files returning nil" do
95+
file_path = create_temp_file("nil_return.exs", "IO.puts(\"side effect\")\nnil")
96+
assert ScriptRunner.run(file_path) == nil
97+
end
98+
99+
test "works with any file extension containing valid Elixir code" do
100+
# .ex files work
101+
ex_file = create_temp_file("test.ex", "\"from .ex\"")
102+
assert ScriptRunner.run(ex_file) == "from .ex"
103+
104+
# .txt files work if they contain Elixir
105+
txt_file = create_temp_file("test.txt", "\"from .txt\"")
106+
assert ScriptRunner.run(txt_file) == "from .txt"
107+
108+
# No extension works too
109+
no_ext = create_temp_file("testfile", "\"no extension\"")
110+
assert ScriptRunner.run(no_ext) == "no extension"
111+
end
112+
113+
test "gives helpful error for non-Elixir content" do
114+
file_path = create_temp_file("not_elixir.txt", "This is just plain text, not Elixir code")
115+
# Should get a syntax error with the filename in the message
116+
exception =
117+
assert_raise SyntaxError, fn ->
118+
ScriptRunner.run(file_path)
119+
end
120+
121+
# The error includes the file path, helping users identify the problem
122+
assert exception.file =~ "not_elixir.txt"
123+
end
124+
125+
test "preserves proper file semantics like __DIR__ and __ENV__" do
126+
file_path = create_temp_file("file_semantics.exs", "{__DIR__, __ENV__.file}")
127+
{dir, file} = ScriptRunner.run(file_path)
128+
129+
# __DIR__ should be the directory containing the file
130+
assert dir == Path.dirname(Path.expand(file_path))
131+
132+
# __ENV__.file should be the absolute path to the file
133+
assert file == Path.expand(file_path)
134+
end
135+
136+
test "supports relative requires within files" do
137+
dir = Path.join(System.tmp_dir(), "require_test_#{:rand.uniform(1000)}")
138+
File.mkdir!(dir)
139+
140+
helper_path = Path.join(dir, "my_helper.exs")
141+
File.write!(helper_path, "defmodule MyHelper do\n def value, do: :from_helper\nend")
142+
143+
main_path = Path.join(dir, "main.exs")
144+
File.write!(main_path, "Code.require_file(\"./my_helper.exs\", __DIR__)\nMyHelper.value()")
145+
146+
on_exit(fn ->
147+
File.rm!(main_path)
148+
File.rm!(helper_path)
149+
File.rmdir!(dir)
150+
end)
151+
152+
assert ScriptRunner.run(main_path) == :from_helper
153+
end
154+
end
155+
37156
describe "github access" do
38157
test "executes with an un-authenticated Tentacat GitHub client by default" do
39158
expect(TentacatMock.ClientMock, :new, fn -> %{auth: nil, endpoint: "github"} end)

0 commit comments

Comments
 (0)