feat: Add :if and :optional
This commit is contained in:
parent
7832ac109e
commit
c8fd5e9f2d
11 changed files with 83 additions and 37 deletions
|
@ -1,4 +1,5 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
plugins: [Styler]
|
||||
]
|
||||
|
|
17
.mash.exs
17
.mash.exs
|
@ -7,26 +7,29 @@ defmodule MashConfig do
|
|||
[
|
||||
%{
|
||||
name: :restore_cache,
|
||||
if: fn _jobs ->
|
||||
System.get_env("CI")
|
||||
end,
|
||||
run: restore_cache()
|
||||
},
|
||||
%{
|
||||
name: :compile_dev,
|
||||
needs: [:restore_cache],
|
||||
needs: [restore_cache: :optional],
|
||||
run: mix("do", ["deps.compile,", "compile"], env: [{"MIX_ENV", "dev"}])
|
||||
},
|
||||
%{
|
||||
name: :test,
|
||||
needs: [:restore_cache],
|
||||
needs: [restore_cache: :optional],
|
||||
run: mix("test")
|
||||
},
|
||||
%{
|
||||
name: :credo,
|
||||
needs: [:restore_cache],
|
||||
needs: [restore_cache: :optional],
|
||||
run: mix("credo", ["--all"])
|
||||
},
|
||||
%{
|
||||
name: :save_cache,
|
||||
needs: [:restore_cache],
|
||||
needs: [:test, :compile_dev],
|
||||
run: save_cache()
|
||||
},
|
||||
%{
|
||||
|
@ -34,6 +37,12 @@ defmodule MashConfig do
|
|||
run: fn _io_pid ->
|
||||
IO.puts("This line should be captured.")
|
||||
end
|
||||
},
|
||||
%{
|
||||
if: false,
|
||||
run: fn _io_pid ->
|
||||
{:error, "This job should not run."}
|
||||
end
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ defmodule MashConfig do
|
|||
run: mix("test")
|
||||
},
|
||||
%{
|
||||
name: :credo,
|
||||
name: :credo,
|
||||
run: mix("credo", ["--all"])
|
||||
},
|
||||
%{
|
||||
|
@ -53,13 +53,15 @@ Each entry in the jobs list must, at a minimum, have two keys:
|
|||
- `:run` -- a function which defines the work for the job to do. We recommend using the helper functions from Mash.Helpers to define your run function, as they ensure that your function handles setup and teardown correctly for many common scenarios, e.g.:
|
||||
- `Mash.Helpers.mix(task_name, args)` -- execute a mix task with the given `task_name` and `args`.
|
||||
- `Mash.Helpers.shell(command, args)` -- run an executable/script named `command` with `args`.
|
||||
- Or you can pass a function with arity 1.
|
||||
- Or you can pass a function with arity 1.
|
||||
- The function should return `:ok` or `{:ok, value}` if the job succeeds, or `{:error, error}` if the job fails.
|
||||
- The argument passed to your function will be the PID of the IO device which is capturing IO. Since we make this PID the "group leader" for your job automatically, you can probably ignore this argument, but you may need to for interfacing with certain libraries that
|
||||
require you to pass an io device PID directly.
|
||||
|
||||
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
|
||||
|
||||
```shell
|
||||
|
|
|
@ -9,7 +9,7 @@ defmodule Mash.Config do
|
|||
|
||||
@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
|
||||
{:error, {:already_started, pid}} ->
|
||||
{:ok, pid}
|
||||
|
|
|
@ -75,9 +75,7 @@ defmodule Mash.Helpers do
|
|||
if File.exists?(path) do
|
||||
cmd("tar", ["-xzf", "#{name}.tar.gz", "--atime-preserve"], io_pid, [])
|
||||
else
|
||||
IO.puts(
|
||||
IO.ANSI.yellow() <> "Warning: cache file #{path} does not exist." <> IO.ANSI.reset()
|
||||
)
|
||||
IO.puts(IO.ANSI.yellow() <> "Warning: cache file #{path} does not exist." <> IO.ANSI.reset())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
defmodule Mash.Job do
|
||||
@moduledoc """
|
||||
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
|
||||
struct(__MODULE__, map)
|
||||
|
@ -11,11 +24,13 @@ defmodule Mash.Job do
|
|||
|
||||
def validate(%__MODULE__{run: run, name: name} = job) do
|
||||
errors =
|
||||
[
|
||||
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))
|
||||
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")
|
||||
],
|
||||
&is_nil(&1)
|
||||
)
|
||||
|
||||
if errors == [] do
|
||||
{:ok, job}
|
||||
|
|
|
@ -22,7 +22,7 @@ defmodule Mash.Runner do
|
|||
|
||||
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}}
|
||||
end
|
||||
|
@ -34,7 +34,7 @@ defmodule Mash.Runner do
|
|||
if any_running?(new_jobs) do
|
||||
tick()
|
||||
else
|
||||
send(parent, {:result, Enum.count(jobs, &(&1.state == :failed))})
|
||||
send(parent, {:result, Enum.filter(jobs, &(&1.state == :failed))})
|
||||
Process.exit(self(), :normal)
|
||||
end
|
||||
|
||||
|
@ -44,6 +44,9 @@ defmodule Mash.Runner do
|
|||
@spec tick_job(%{state: job_state()}, [Mash.Job.t()]) :: Mash.Job.t()
|
||||
def tick_job(%{state: :to_run} = job, jobs) do
|
||||
cond do
|
||||
!if?(job, jobs) ->
|
||||
%{job | state: :skipped}
|
||||
|
||||
ready?(job, jobs) ->
|
||||
%{job | state: :running}
|
||||
|
||||
|
@ -56,15 +59,14 @@ defmodule Mash.Runner do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
%{job | task: task, io_pid: io_pid}
|
||||
end
|
||||
|
||||
def tick_job(%{name: name, state: :running, task: task, io_pid: io_pid} = job, _jobs)
|
||||
when is_struct(task, Task) do
|
||||
def tick_job(%{name: name, state: :running, task: task, io_pid: io_pid} = job, _jobs) when is_struct(task, Task) do
|
||||
flush_io(name, io_pid)
|
||||
|
||||
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 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
|
||||
Enum.any?(jobs, &(&1.state == :running))
|
||||
end
|
||||
|
@ -93,18 +98,29 @@ defmodule Mash.Runner do
|
|||
end
|
||||
|
||||
defp ready?(%{needs: needs}, jobs) do
|
||||
Enum.all?(needs, fn needed_job_name ->
|
||||
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
|
||||
Enum.all?(needs, fn
|
||||
{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
|
||||
|
||||
defp skippable?(%{needs: needs}, jobs) do
|
||||
Enum.any?(needs, fn needed_job_name ->
|
||||
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
|
||||
Enum.any?(needs, fn
|
||||
{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
|
||||
|
||||
|
@ -120,7 +136,7 @@ defmodule Mash.Runner do
|
|||
|
||||
StringIO.close(job.io_pid)
|
||||
new_state = interpret_result(result)
|
||||
IO.puts("Finishing #{job_name} -- #{new_state}")
|
||||
IO.puts("[runner] Finishing #{job_name} -- #{new_state}")
|
||||
|
||||
new_job = %{
|
||||
job
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
defmodule Mix.Tasks.Mash do
|
||||
@shortdoc "Run Mash jobs."
|
||||
|
||||
@moduledoc """
|
||||
Task to run Mash jobs.
|
||||
"""
|
||||
@shortdoc "Run Mash jobs."
|
||||
|
||||
use Mix.Task
|
||||
|
||||
def run(_args) do
|
||||
{:ok, _pid} = Mash.Runner.start_link()
|
||||
|
||||
receive do
|
||||
{:result, failure_count} when failure_count > 0 ->
|
||||
Mix.raise("#{failure_count} job(s) failed.", exit_status: 1)
|
||||
|
||||
{:result, 0} ->
|
||||
{:result, []} ->
|
||||
: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
|
||||
|
|
5
mix.exs
5
mix.exs
|
@ -4,7 +4,7 @@ defmodule Mash.MixProject do
|
|||
def project do
|
||||
[
|
||||
app: :mash,
|
||||
version: "0.1.2",
|
||||
version: "0.2.0",
|
||||
elixir: "~> 1.14",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
|
@ -31,7 +31,8 @@ defmodule Mash.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{: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_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
]
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -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_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"},
|
||||
"styler": {:hex, :styler, "0.8.4", "70580b48e728c81e0ad02f9de26839da35281547fb2369e397d84eb7840ec6d8", [:mix], [], "hexpm", "4525686e49d1784a53c0964f1602c33250caf7646c14a0e1c7dddca806337428"},
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
defmodule MashTest do
|
||||
use ExUnit.Case
|
||||
|
||||
doctest Mash
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue