|
| 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 |
0 commit comments