feat: Add :if and :optional

This commit is contained in:
Robert Prehn 2023-08-17 20:13:47 +00:00
parent 7832ac109e
commit c8fd5e9f2d
No known key found for this signature in database
11 changed files with 83 additions and 37 deletions

View file

@ -1,4 +1,5 @@
# Used by "mix format" # Used by "mix format"
[ [
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
plugins: [Styler]
] ]

View file

@ -7,26 +7,29 @@ defmodule MashConfig do
[ [
%{ %{
name: :restore_cache, name: :restore_cache,
if: fn _jobs ->
System.get_env("CI")
end,
run: restore_cache() run: restore_cache()
}, },
%{ %{
name: :compile_dev, name: :compile_dev,
needs: [:restore_cache], needs: [restore_cache: :optional],
run: mix("do", ["deps.compile,", "compile"], env: [{"MIX_ENV", "dev"}]) run: mix("do", ["deps.compile,", "compile"], env: [{"MIX_ENV", "dev"}])
}, },
%{ %{
name: :test, name: :test,
needs: [:restore_cache], needs: [restore_cache: :optional],
run: mix("test") run: mix("test")
}, },
%{ %{
name: :credo, name: :credo,
needs: [:restore_cache], needs: [restore_cache: :optional],
run: mix("credo", ["--all"]) run: mix("credo", ["--all"])
}, },
%{ %{
name: :save_cache, name: :save_cache,
needs: [:restore_cache], needs: [:test, :compile_dev],
run: save_cache() run: save_cache()
}, },
%{ %{
@ -34,6 +37,12 @@ defmodule MashConfig do
run: fn _io_pid -> run: fn _io_pid ->
IO.puts("This line should be captured.") IO.puts("This line should be captured.")
end end
},
%{
if: false,
run: fn _io_pid ->
{:error, "This job should not run."}
end
} }
] ]
end end

View file

@ -60,6 +60,8 @@ Each entry in the jobs list must, at a minimum, have two keys:
Optionally, a job may also contain a key `needs` whose value is an array of other jobs that must pass before this job will be run. Optionally, a job may also contain a key `needs` whose value is an array of other jobs that must pass before this job will be run.
See [the docs for Mash.Job](https://hexdocs.pm/mash/Mash.Job.html) for more details, including conditional execution.
## Running ## Running
```shell ```shell

View file

@ -9,7 +9,7 @@ defmodule Mash.Config do
@callback jobs() :: [%{name: atom(), run: function()}] @callback jobs() :: [%{name: atom(), run: function()}]
def start_link() do def start_link do
case Agent.start_link(&compile_config_module!/0, name: __MODULE__) do case Agent.start_link(&compile_config_module!/0, name: __MODULE__) do
{:error, {:already_started, pid}} -> {:error, {:already_started, pid}} ->
{:ok, pid} {:ok, pid}

View file

@ -75,9 +75,7 @@ defmodule Mash.Helpers do
if File.exists?(path) do if File.exists?(path) do
cmd("tar", ["-xzf", "#{name}.tar.gz", "--atime-preserve"], io_pid, []) cmd("tar", ["-xzf", "#{name}.tar.gz", "--atime-preserve"], io_pid, [])
else else
IO.puts( IO.puts(IO.ANSI.yellow() <> "Warning: cache file #{path} does not exist." <> IO.ANSI.reset())
IO.ANSI.yellow() <> "Warning: cache file #{path} does not exist." <> IO.ANSI.reset()
)
end end
end end
end end

View file

@ -1,9 +1,22 @@
defmodule Mash.Job do defmodule Mash.Job do
@moduledoc """ @moduledoc """
Structure that represents the configuration and status of one Mash job. Structure that represents the configuration and status of one Mash job.
Keys:
- `:name`: an atom which is the name of the job. It should be unique in the job list.
- `:if`: either a function w/ arity 1 or a plain value. The job will be run if the value is truthy \
or the function returns true. Otherwise it will be skipped. The argument to an if function is the runner's \
job list.
- `:needs`: a list of job name atoms which are required before running this job. If a dependency fails or is skipped, \
this job will be skipped. You can also include {job_name, :optional} to indicate an optional dependency. \
Optional dependencies do not block a job from running if they are skipped, only if they fail.
- `:run`: a function with arity 1 that is the code to be executed for the job. See `Mash.Helpers` for shortcut functions.\
The argument to the run function is the PID of the IO device which is capturing the output of this job. You\
probably don't need this, but you might need it for certain libaries that need to be passed an explicit IO PID.
- `:env`: an array of {string, string} pairs which represent ENV variables (name first, then value).
""" """
defstruct name: nil, needs: [], run: nil, env: [], state: nil, task: nil, io_pid: nil defstruct name: nil, if: true, needs: [], run: nil, env: [], state: nil, task: nil, io_pid: nil
def from_map(map) do def from_map(map) do
struct(__MODULE__, map) struct(__MODULE__, map)
@ -11,11 +24,13 @@ defmodule Mash.Job do
def validate(%__MODULE__{run: run, name: name} = job) do def validate(%__MODULE__{run: run, name: name} = job) do
errors = errors =
[ Enum.reject(
if(!is_function(run, 1), do: "run is required and must be a function with arity 1"), [
if(!is_atom(name), do: "name is required and must be an atom") if(!is_function(run, 1), do: "run is required and must be a function with arity 1"),
] if(!is_atom(name), do: "name is required and must be an atom")
|> Enum.reject(&is_nil(&1)) ],
&is_nil(&1)
)
if errors == [] do if errors == [] do
{:ok, job} {:ok, job}

View file

@ -22,7 +22,7 @@ defmodule Mash.Runner do
tick() tick()
jobs = Mash.Config.jobs() |> Enum.map(&Map.put(&1, :state, :to_run)) jobs = Enum.map(Mash.Config.jobs(), &Map.put(&1, :state, :to_run))
{:ok, {parent, jobs}} {:ok, {parent, jobs}}
end end
@ -34,7 +34,7 @@ defmodule Mash.Runner do
if any_running?(new_jobs) do if any_running?(new_jobs) do
tick() tick()
else else
send(parent, {:result, Enum.count(jobs, &(&1.state == :failed))}) send(parent, {:result, Enum.filter(jobs, &(&1.state == :failed))})
Process.exit(self(), :normal) Process.exit(self(), :normal)
end end
@ -44,6 +44,9 @@ defmodule Mash.Runner do
@spec tick_job(%{state: job_state()}, [Mash.Job.t()]) :: Mash.Job.t() @spec tick_job(%{state: job_state()}, [Mash.Job.t()]) :: Mash.Job.t()
def tick_job(%{state: :to_run} = job, jobs) do def tick_job(%{state: :to_run} = job, jobs) do
cond do cond do
!if?(job, jobs) ->
%{job | state: :skipped}
ready?(job, jobs) -> ready?(job, jobs) ->
%{job | state: :running} %{job | state: :running}
@ -56,15 +59,14 @@ defmodule Mash.Runner do
end end
def tick_job(%{name: name, state: :running, task: nil} = job, _jobs) do def tick_job(%{name: name, state: :running, task: nil} = job, _jobs) do
IO.puts("Running #{name}") IO.puts("[runner] Running #{name}")
{task, io_pid} = Mash.Execution.run(job) {task, io_pid} = Mash.Execution.run(job)
%{job | task: task, io_pid: io_pid} %{job | task: task, io_pid: io_pid}
end end
def tick_job(%{name: name, state: :running, task: task, io_pid: io_pid} = job, _jobs) def tick_job(%{name: name, state: :running, task: task, io_pid: io_pid} = job, _jobs) when is_struct(task, Task) do
when is_struct(task, Task) do
flush_io(name, io_pid) flush_io(name, io_pid)
job job
@ -72,6 +74,9 @@ defmodule Mash.Runner do
def tick_job(%{state: state} = job, _jobs) when state in [:passed, :failed, :skipped], do: job def tick_job(%{state: state} = job, _jobs) when state in [:passed, :failed, :skipped], do: job
def if?(%{if: if_fun}, jobs) when is_function(if_fun), do: !!if_fun.(jobs)
def if?(%{if: if_val}, _jobs), do: !!if_val
def any_running?(jobs) do def any_running?(jobs) do
Enum.any?(jobs, &(&1.state == :running)) Enum.any?(jobs, &(&1.state == :running))
end end
@ -93,18 +98,29 @@ defmodule Mash.Runner do
end end
defp ready?(%{needs: needs}, jobs) do defp ready?(%{needs: needs}, jobs) do
Enum.all?(needs, fn needed_job_name -> Enum.all?(needs, fn
needed_job = Enum.find(jobs, &(&1.name == needed_job_name)) {needed_job_name, :optional} ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state == :passed needed_job.state in [:skipped, :passed]
needed_job_name ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state == :passed
end) end)
end end
defp skippable?(%{needs: needs}, jobs) do defp skippable?(%{needs: needs}, jobs) do
Enum.any?(needs, fn needed_job_name -> Enum.any?(needs, fn
needed_job = Enum.find(jobs, &(&1.name == needed_job_name)) {needed_job_name, :optional} ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state == :failed
needed_job.state in [:failed, :skipped] needed_job_name ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state in [:failed, :skipped]
end) end)
end end
@ -120,7 +136,7 @@ defmodule Mash.Runner do
StringIO.close(job.io_pid) StringIO.close(job.io_pid)
new_state = interpret_result(result) new_state = interpret_result(result)
IO.puts("Finishing #{job_name} -- #{new_state}") IO.puts("[runner] Finishing #{job_name} -- #{new_state}")
new_job = %{ new_job = %{
job job

View file

@ -1,20 +1,22 @@
defmodule Mix.Tasks.Mash do defmodule Mix.Tasks.Mash do
@shortdoc "Run Mash jobs."
@moduledoc """ @moduledoc """
Task to run Mash jobs. Task to run Mash jobs.
""" """
@shortdoc "Run Mash jobs."
use Mix.Task use Mix.Task
def run(_args) do def run(_args) do
{:ok, _pid} = Mash.Runner.start_link() {:ok, _pid} = Mash.Runner.start_link()
receive do receive do
{:result, failure_count} when failure_count > 0 -> {:result, []} ->
Mix.raise("#{failure_count} job(s) failed.", exit_status: 1)
{:result, 0} ->
:ok :ok
{:result, failures} ->
failure_list = Enum.map_join(failures, "\n", fn %{name: name} -> "- #{name}" end)
failure_count = Enum.count(failures)
Mix.raise("#{failure_count} job(s) failed.\n#{failure_list}", exit_status: 1)
end end
end end
end end

View file

@ -4,7 +4,7 @@ defmodule Mash.MixProject do
def project do def project do
[ [
app: :mash, app: :mash,
version: "0.1.2", version: "0.2.0",
elixir: "~> 1.14", elixir: "~> 1.14",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
deps: deps(), deps: deps(),
@ -31,7 +31,8 @@ defmodule Mash.MixProject do
defp deps do defp deps do
[ [
{:ex_doc, "~> 0.30.4", only: :dev}, {:ex_doc, "~> 0.30.4", only: :dev},
{:credo, "~> 1.7"} {:credo, "~> 1.7"},
{:styler, "~> 0.8.4"}
# {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
] ]

View file

@ -9,4 +9,5 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"styler": {:hex, :styler, "0.8.4", "70580b48e728c81e0ad02f9de26839da35281547fb2369e397d84eb7840ec6d8", [:mix], [], "hexpm", "4525686e49d1784a53c0964f1602c33250caf7646c14a0e1c7dddca806337428"},
} }

View file

@ -1,4 +1,5 @@
defmodule MashTest do defmodule MashTest do
use ExUnit.Case use ExUnit.Case
doctest Mash doctest Mash
end end