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"
|
# 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,
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
mix.exs
5
mix.exs
|
@ -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"}
|
||||||
]
|
]
|
||||||
|
|
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_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"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule MashTest do
|
defmodule MashTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
|
|
||||||
doctest Mash
|
doctest Mash
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue