Skip to content

Commit 5fc7cc3

Browse files
committed
Add Mix task UpdateExamplesWorkflow
1 parent 8034478 commit 5fc7cc3

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule Mix.Tasks.E2e.UpdateExamplesWorkflow do
2+
use Mix.Task
3+
alias ElixirScript.E2e
4+
alias ElixirScript.E2e.Entry
5+
6+
@shortdoc "Updates the examples workflow. Use --check to fail if file needs updating"
7+
8+
@workflow_file ".github/workflows/examples.yml"
9+
10+
@spec run([String.t()]) :: :ok
11+
def run(args) do
12+
check_only? = "--check" in args
13+
new_content = E2e.read_test_file() |> generate_workflow()
14+
15+
case read_workflow_file() do
16+
{:ok, current_content} when current_content == new_content ->
17+
Mix.shell().info("Workflow file is up to date")
18+
19+
{:ok, _} when check_only? ->
20+
Mix.raise(
21+
"Workflow file is not up to date. Run `mix e2e.update_examples_workflow` to update it"
22+
)
23+
24+
{:ok, _} ->
25+
# If file exists but content is different, write the new content
26+
write_workflow_file(new_content)
27+
Mix.shell().info("Workflow file updated")
28+
29+
{:error, :enoent} when check_only? ->
30+
# If file doesn't exist and check_only is true, raise an error
31+
Mix.raise(
32+
"Workflow file does not exist. Run `mix e2e.update_examples_workflow` to create it"
33+
)
34+
35+
{:error, :enoent} ->
36+
# If file doesn't exist, write the new content as a new file
37+
write_workflow_file(new_content)
38+
Mix.shell().info("Workflow file created")
39+
40+
{:error, reason} ->
41+
# Handle other errors
42+
Mix.raise("Failed to read workflow file: #{reason}")
43+
end
44+
end
45+
46+
def read_workflow_file do
47+
File.read(@workflow_file)
48+
end
49+
50+
def write_workflow_file(content) do
51+
File.write!(@workflow_file, content)
52+
end
53+
54+
def generate_workflow(entries) do
55+
jobs =
56+
Enum.map(entries, fn entry -> generate_job(entry) end)
57+
|> Enum.join("\n")
58+
|> indent_string(2)
59+
60+
"""
61+
# CI output from these examples are available here:
62+
# https://github.com/gaggle/elixir_script/actions/workflows/examples.yml?query=branch%3Amain
63+
#
64+
# ℹ️ This file is automatically generated via `mix e2e.update_examples_workflow`
65+
66+
name: Examples
67+
68+
on:
69+
push:
70+
paths:
71+
- .github/workflows/examples.yml
72+
release:
73+
types:
74+
- "created"
75+
workflow_dispatch:
76+
77+
jobs:
78+
""" <> jobs
79+
end
80+
81+
defp generate_job(%Entry{} = entry) do
82+
pre_indented_script_lines = entry.script |> String.trim_trailing |> dedent_string |> indent_string(10)
83+
# ↑
84+
# No trailing empty lines because we tightly control how script-lines are placed within the template
85+
# ↑↑
86+
# In the job template below "script" is indented 8, so the script itself needs 10 indents
87+
88+
"""
89+
#{entry.id}:
90+
runs-on: ubuntu-latest
91+
steps:
92+
- uses: gaggle/elixir_script@v0
93+
id: script
94+
with:
95+
script: |
96+
#{pre_indented_script_lines}
97+
98+
- name: Get result
99+
run: echo "\${{steps.script.outputs.result}}"
100+
"""
101+
end
102+
103+
@spec indent_string(String.t(), non_neg_integer()) :: String.t()
104+
defp indent_string(str, indent) do
105+
indentation = String.duplicate(" ", indent)
106+
107+
str
108+
|> String.split(~r/\n/)
109+
|> Enum.map(fn
110+
line ->
111+
case String.trim(line) do
112+
"" -> line
113+
_ -> indentation <> line
114+
end
115+
end)
116+
|> Enum.join("\n")
117+
end
118+
119+
@spec dedent_string(String.t()) :: String.t()
120+
defp dedent_string(str) do
121+
lines = String.split(str, "\n")
122+
smallest_indent =
123+
lines
124+
|> Enum.reject(&String.trim(&1) == "") # Ignore empty or whitespace-only lines
125+
|> Enum.map(&String.length(Regex.replace(~r/^(\s*).*$/, &1, "\\1")))
126+
|> Enum.min()
127+
|> Kernel.||(0) # Default to 0 if the list is empty
128+
129+
lines
130+
|> Enum.map(fn line ->
131+
slice_length = Enum.min([String.length(line), smallest_indent])
132+
String.slice(line, slice_length..-1)
133+
end)
134+
|> Enum.join("\n")
135+
end
136+
end
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
defmodule Mix.Tasks.E2e.UpdateExamplesWorkflowTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ElixirScript.E2e.Entry
5+
alias Mix.Tasks.E2e.UpdateExamplesWorkflow
6+
7+
describe "generate_workflow/1" do
8+
test "generates workflow content from entries" do
9+
entry = %Entry{
10+
id: "io-visible-in-logs-and-return-value-available-via-outputs",
11+
script: """
12+
IO.puts("Hello world")
13+
"return"
14+
"""
15+
}
16+
17+
actual_output = UpdateExamplesWorkflow.generate_workflow([entry])
18+
19+
expected_output =
20+
"""
21+
# CI output from these examples are available here:
22+
# https://github.com/gaggle/elixir_script/actions/workflows/examples.yml?query=branch%3Amain
23+
#
24+
# ℹ️ This file is automatically generated via `mix e2e.update_examples_workflow`
25+
26+
name: Examples
27+
28+
on:
29+
push:
30+
paths:
31+
- .github/workflows/examples.yml
32+
release:
33+
types:
34+
- "created"
35+
workflow_dispatch:
36+
37+
jobs:
38+
io-visible-in-logs-and-return-value-available-via-outputs:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: gaggle/elixir_script@v0
42+
id: script
43+
with:
44+
script: |
45+
IO.puts("Hello world")
46+
"return"
47+
48+
- name: Get result
49+
run: echo "${{steps.script.outputs.result}}"
50+
"""
51+
52+
assert_multiline_string_equality(actual_output, expected_output)
53+
end
54+
55+
test "can handle poorly-indented script" do
56+
entry = %Entry{
57+
id: "io-visible-in-logs-and-return-value-available-via-outputs",
58+
script: """
59+
defmodule Foo do
60+
def bar, do: "bar"
61+
end
62+
Foo.bar()
63+
"""
64+
}
65+
66+
actual_output = UpdateExamplesWorkflow.generate_workflow([entry])
67+
68+
expected_output =
69+
"""
70+
# CI output from these examples are available here:
71+
# https://github.com/gaggle/elixir_script/actions/workflows/examples.yml?query=branch%3Amain
72+
#
73+
# ℹ️ This file is automatically generated via `mix e2e.update_examples_workflow`
74+
75+
name: Examples
76+
77+
on:
78+
push:
79+
paths:
80+
- .github/workflows/examples.yml
81+
release:
82+
types:
83+
- "created"
84+
workflow_dispatch:
85+
86+
jobs:
87+
io-visible-in-logs-and-return-value-available-via-outputs:
88+
runs-on: ubuntu-latest
89+
steps:
90+
- uses: gaggle/elixir_script@v0
91+
id: script
92+
with:
93+
script: |
94+
defmodule Foo do
95+
def bar, do: "bar"
96+
end
97+
Foo.bar()
98+
99+
- name: Get result
100+
run: echo "${{steps.script.outputs.result}}"
101+
"""
102+
103+
assert_multiline_string_equality(actual_output, expected_output)
104+
end
105+
end
106+
107+
defp assert_multiline_string_equality(actual, expected) do
108+
unless actual == expected do
109+
IO.puts("Actual:\n#{actual}")
110+
IO.puts("Expected:\n#{expected}")
111+
end
112+
113+
assert actual == expected
114+
end
115+
end

0 commit comments

Comments
 (0)