Skip to content

Commit d1e248c

Browse files
committed
E2E test file-based scripts in workflows
1 parent 2d2d45d commit d1e248c

File tree

8 files changed

+381
-47
lines changed

8 files changed

+381
-47
lines changed

.github/actions/run-e2e/action.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ runs:
1616
run: |
1717
sed -i 's/\( image: \).*/\1${{ inputs.image-name }}/' action.yml
1818
shell: bash
19+
- name: Create script files
20+
if: ${{ matrix.file_content }}
21+
shell: bash
22+
run: |
23+
echo "Creating files for test: ${{ matrix.name }}"
24+
# Use jq to iterate over the JSON object
25+
echo '${{ toJSON(matrix.file_content) }}' | jq -r 'to_entries[] | @json' | while IFS= read -r entry; do
26+
path=$(echo "$entry" | jq -r '.key')
27+
content=$(echo "$entry" | jq -r '.value')
28+
29+
# Create directory if needed
30+
dir=$(dirname "$path")
31+
mkdir -p "$dir"
32+
33+
# Write the file content
34+
echo "$content" > "$path"
35+
echo "Created file: $path"
36+
done
1937
- name: Run gaggle/elixir_script
2038
id: run
2139
uses: ./
2240
with:
23-
script: |
24-
${{ matrix.script }}
41+
script: ${{ matrix.script }}
2542
- name: Assert output
2643
if: ${{ !!matrix.expected }}
2744
run: |

.github/workflows/ci-cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
with:
8080
otp-version: ${{ needs.version.outputs.otp-version }}
8181
elixir-version: ${{ needs.version.outputs.elixir-version }}
82-
- run: bin/audit --skip-check-image
82+
- run: pkgx +invisible-island.net/ncurses -- bin/audit --skip-check-image
8383

8484
build:
8585
runs-on: ubuntu-latest

.github/workflows/examples.yml

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ jobs:
3333
3434
- name: Get result
3535
run: echo "${{steps.script.outputs.result}}"
36-
3736
io-is-visible-in-logs:
3837
runs-on: ubuntu-latest
3938
steps:
@@ -45,7 +44,6 @@ jobs:
4544
4645
- name: Get result
4746
run: echo "${{steps.script.outputs.result}}"
48-
4947
event-context-is-available:
5048
runs-on: ubuntu-latest
5149
steps:
@@ -58,7 +56,6 @@ jobs:
5856
5957
- name: Get result
6058
run: echo "${{steps.script.outputs.result}}"
61-
6259
the-entire-context-can-be-inspected:
6360
runs-on: ubuntu-latest
6461
steps:
@@ -70,7 +67,6 @@ jobs:
7067
7168
- name: Get result
7269
run: echo "${{steps.script.outputs.result}}"
73-
7470
multiline-scripts-are-possible:
7571
runs-on: ubuntu-latest
7672
steps:
@@ -83,7 +79,6 @@ jobs:
8379
8480
- name: Get result
8581
run: echo "${{steps.script.outputs.result}}"
86-
8782
oh-hi-mark-greeter:
8883
runs-on: ubuntu-latest
8984
steps:
@@ -98,7 +93,6 @@ jobs:
9893
9994
- name: Get result
10095
run: echo "${{steps.script.outputs.result}}"
101-
10296
can-use-the-github-api-via-tentacat:
10397
runs-on: ubuntu-latest
10498
steps:
@@ -111,7 +105,6 @@ jobs:
111105
112106
- name: Get result
113107
run: echo "${{steps.script.outputs.result}}"
114-
115108
can-grab-information-via-the-github-api:
116109
runs-on: ubuntu-latest
117110
steps:
@@ -124,7 +117,6 @@ jobs:
124117
125118
- name: Get result
126119
run: echo "${{steps.script.outputs.result}}"
127-
128120
can-interact-with-repositories-via-github-api:
129121
runs-on: ubuntu-latest
130122
steps:
@@ -137,3 +129,138 @@ jobs:
137129
138130
- name: Get result
139131
run: echo "${{steps.script.outputs.result}}"
132+
file-scripts-can-define-and-use-modules:
133+
runs-on: ubuntu-latest
134+
steps:
135+
- name: Create script files
136+
run: |
137+
mkdir -p ./.github/scripts
138+
cat > ./.github/scripts/pr_analyzer.exs << 'EOFMARKER'
139+
defmodule PRAnalyzer do
140+
def analyze(context) do
141+
%{
142+
event: context.event_name,
143+
workflow: context.workflow,
144+
job: context.job,
145+
ref: context.ref
146+
}
147+
end
148+
149+
def format_message(analysis) do
150+
"PR Analysis: event=#{analysis.event}"
151+
end
152+
end
153+
154+
# Use the module to analyze and format
155+
analysis = PRAnalyzer.analyze(context)
156+
PRAnalyzer.format_message(analysis)
157+
EOFMARKER
158+
159+
- uses: gaggle/elixir_script@v0
160+
id: script
161+
with:
162+
script: ./.github/scripts/pr_analyzer.exs
163+
164+
- name: Get result
165+
run: echo "${{steps.script.outputs.result}}"
166+
file-scripts-can-use-relative-require-for-helper-modules:
167+
runs-on: ubuntu-latest
168+
steps:
169+
- name: Create script files
170+
run: |
171+
mkdir -p ./scripts
172+
cat > ./scripts/helpers.exs << 'EOFMARKER'
173+
defmodule Helpers do
174+
def format_workflow(_context) do
175+
"Script loaded from: ./scripts/main.exs"
176+
end
177+
end
178+
EOFMARKER
179+
mkdir -p ./scripts
180+
cat > ./scripts/main.exs << 'EOFMARKER'
181+
Code.require_file("helpers.exs", __DIR__)
182+
Helpers.format_workflow(context)
183+
EOFMARKER
184+
185+
- uses: gaggle/elixir_script@v0
186+
id: script
187+
with:
188+
script: ./scripts/main.exs
189+
190+
- name: Get result
191+
run: echo "${{steps.script.outputs.result}}"
192+
bootstrap-pattern-delegates-to-testable-module:
193+
runs-on: ubuntu-latest
194+
steps:
195+
- name: Create script files
196+
run: |
197+
mkdir -p .
198+
cat > main.ex << 'EOFMARKER'
199+
defmodule Main do
200+
def run(_context) do
201+
"Bootstrap test passed!"
202+
end
203+
end
204+
EOFMARKER
205+
206+
- uses: gaggle/elixir_script@v0
207+
id: script
208+
with:
209+
script: |
210+
# Bootstrap: load and run the main module
211+
Code.require_file("main.ex", ".")
212+
Main.run(context)
213+
214+
- name: Get result
215+
run: echo "${{steps.script.outputs.result}}"
216+
bootstrap-with-complex-module-structure:
217+
runs-on: ubuntu-latest
218+
steps:
219+
- name: Create script files
220+
run: |
221+
mkdir -p lib
222+
cat > lib/analyzer.ex << 'EOFMARKER'
223+
defmodule Analyzer do
224+
def analyze_event(context) do
225+
%{
226+
type: context.event_name,
227+
branch: extract_branch(context.ref)
228+
}
229+
end
230+
231+
defp extract_branch(ref) do
232+
ref |> String.split("/") |> List.last()
233+
end
234+
end
235+
EOFMARKER
236+
mkdir -p lib
237+
cat > lib/app.ex << 'EOFMARKER'
238+
defmodule App do
239+
def start(context, _client) do
240+
context
241+
|> Analyzer.analyze_event()
242+
|> Formatter.format_analysis()
243+
end
244+
end
245+
EOFMARKER
246+
mkdir -p lib
247+
cat > lib/formatter.ex << 'EOFMARKER'
248+
defmodule Formatter do
249+
def format_analysis(analysis) do
250+
"Event type: #{String.capitalize(analysis.type)}"
251+
end
252+
end
253+
EOFMARKER
254+
255+
- uses: gaggle/elixir_script@v0
256+
id: script
257+
with:
258+
script: |
259+
# Minimal bootstrap that loads and runs the application
260+
Code.require_file("lib/analyzer.ex", ".")
261+
Code.require_file("lib/formatter.ex", ".")
262+
Code.require_file("lib/app.ex", ".")
263+
App.start(context, client)
264+
265+
- name: Get result
266+
run: echo "${{steps.script.outputs.result}}"

lib/e2e.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule ElixirScript.E2e.Entry do
33
A struct representing the data for an E2E test
44
"""
55

6-
defstruct id: nil, name: nil, script: nil, file: nil, expected: nil
6+
defstruct id: nil, name: nil, script: nil, file_content: nil, expected: nil
77
end
88

99
defmodule ElixirScript.E2e do
@@ -25,18 +25,18 @@ defmodule ElixirScript.E2e do
2525
defp process_entry(entry) do
2626
name = Map.fetch!(entry, :name)
2727
script = Map.get(entry, :script)
28-
file = Map.get(entry, :file)
28+
file_content = Map.get(entry, :file_content)
2929
expected = Map.get(entry, :expected)
3030

31-
if !script && !file do
32-
raise(KeyError, "key :script or :file not found in: #{inspect(entry)}")
31+
if !script do
32+
raise(KeyError, "key :script not found in: #{inspect(entry)}")
3333
end
3434

3535
%Entry{
3636
id: slugify(name),
3737
name: name,
3838
script: script,
39-
file: file,
39+
file_content: file_content,
4040
expected: expected
4141
}
4242
end

lib/mix/tasks/e2e/set_github_matrix.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ defmodule Mix.Tasks.E2e.SetGithubMatrix do
4040

4141
# Converts an `Entry` struct to a map for JSON encoding.
4242
defp entry_to_map_for_json(%Entry{} = entry) do
43-
Map.take(entry, [:name, :script, :expected])
43+
Map.take(entry, [:name, :script, :expected, :file_content])
4444
end
4545

4646
# Encodes the provided entries into a JSON structure for GitHub Actions matrix.

lib/mix/tasks/e2e/update_examples_workflow.ex

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@ defmodule Mix.Tasks.E2e.UpdateExamplesWorkflow do
5555
end
5656

5757
def generate_workflow(entries) do
58-
jobs =
59-
Enum.map_join(entries, "\n", fn entry -> generate_job(entry) end)
60-
|> indent_string(2)
58+
jobs = Enum.map_join(entries, &generate_job/1)
6159

6260
"""
6361
# CI output from these examples are available here:
@@ -85,29 +83,67 @@ defmodule Mix.Tasks.E2e.UpdateExamplesWorkflow do
8583
end
8684

8785
defp generate_job(%Entry{} = entry) do
88-
pre_indented_script_lines =
89-
entry.script |> String.trim_trailing() |> dedent_string |> indent_string(10)
86+
action_ref = "gaggle/elixir_script" <> "@" <> "v0"
9087

91-
# ↑
92-
# No trailing empty lines because we tightly control how script-lines are placed within the template
93-
# ↑↑
94-
# In the job template below "script" is indented 8, so the script itself needs 10 indents
88+
file_creation_step = generate_file_creation_step(entry.file_content)
89+
script_yaml = format_script_yaml(entry.script)
9590

9691
"""
97-
#{entry.id}:
98-
runs-on: ubuntu-latest
99-
steps:
100-
- uses: gaggle/elixir_script@v0
101-
id: script
102-
with:
103-
script: |
104-
#{pre_indented_script_lines}
105-
106-
- name: Get result
107-
run: echo "\${{steps.script.outputs.result}}"
92+
#{entry.id}:
93+
runs-on: ubuntu-latest
94+
steps:
95+
#{file_creation_step} - uses: #{action_ref}
96+
id: script
97+
with:
98+
#{script_yaml}
99+
100+
- name: Get result
101+
run: echo "\${{steps.script.outputs.result}}"
108102
"""
109103
end
110104

105+
defp generate_file_creation_step(nil), do: ""
106+
107+
defp generate_file_creation_step(file_content) do
108+
file_steps =
109+
file_content
110+
|> Enum.map_join("\n", fn {path, content} ->
111+
dir = Path.dirname(path)
112+
trimmed_content = String.trim_trailing(content)
113+
indented_content = indent_string(trimmed_content, 14)
114+
115+
" mkdir -p #{dir}\n" <>
116+
" cat > #{path} << 'EOFMARKER'\n" <>
117+
"#{indented_content}\n" <>
118+
" EOFMARKER"
119+
end)
120+
121+
"""
122+
- name: Create script files
123+
run: |
124+
#{file_steps}
125+
126+
"""
127+
end
128+
129+
defp format_script_yaml(script) do
130+
trimmed_script = String.trim(script)
131+
132+
# Check if it's a file path (same logic as ScriptRunner.is_file_path?)
133+
is_file_path = String.starts_with?(trimmed_script, ["./", "../", "/"])
134+
135+
if is_file_path do
136+
# File path - use inline format
137+
" script: #{trimmed_script}"
138+
else
139+
# Inline code - use pipe format with proper indentation
140+
indented_script =
141+
script |> String.trim_trailing() |> dedent_string |> indent_string(12)
142+
143+
" script: |\n#{indented_script}"
144+
end
145+
end
146+
111147
@spec indent_string(String.t(), non_neg_integer()) :: String.t()
112148
defp indent_string(str, indent) do
113149
indentation = String.duplicate(" ", indent)

0 commit comments

Comments
 (0)