defmodule Mix.Tasks.HostKit.Runs do @moduledoc """ Lists minimal HostKit run records. """ use Mix.Task alias Mix.Tasks.HostKit.Options @shortdoc "List tracked HostKit runs" @impl true def run(args) do Mix.Task.run("app.start") {opts, positional} = OptionParser.parse!(args, strict: [ local: :boolean, host: :string, remote: :string, user: :string, port: :integer, identity_file: :string, password: :string, password_env: :string, silently_accept_hosts: :boolean, sudo: :boolean, require: :keep, runs_root: :string, format: :string, verbose: :boolean, latest: :boolean, id: :string, prune: :boolean, keep: :integer ] ) project = load_project(opts, positional) Options.with_target_opts(opts, project, fn target_opts -> opts |> run_opts(target_opts) |> run_command(opts) end) end defp run_command(run_opts, opts) do if Keyword.get(opts, :prune, false), do: prune_runs(run_opts, opts), else: list_runs(run_opts, opts) end defp prune_runs(run_opts, opts) do case HostKit.RunRecord.prune(run_opts, keep: Keyword.get(opts, :keep, 20)) do {:ok, pruned} -> IO.puts("pruned #{length(pruned)} HostKit run(s)") {:error, reason} -> Mix.raise("could not prune HostKit runs: #{inspect(reason)}") end end defp list_runs(run_opts, opts) do case load_records(opts, run_opts) do {:ok, records} -> IO.puts(format_records(records, opts)) {:error, reason} -> Mix.raise("could not list HostKit runs: #{inspect(reason)}") end end defp load_project(opts, positional) do if Keyword.has_key?(opts, :host) do path = List.first(positional) || "infra/config.exs" HostKit.load!(path, require: Keyword.get_values(opts, :require)) end end defp run_opts(opts, target_opts) do target_opts |> Options.expand_target_opts() |> put_present(:hostkit_runs_root, Keyword.get(opts, :runs_root)) end defp load_records(opts, run_opts) do cond do id = Keyword.get(opts, :id) -> with {:ok, record} <- HostKit.RunRecord.load(id, run_opts), do: {:ok, [record]} Keyword.get(opts, :latest, false) -> with {:ok, record} <- HostKit.RunRecord.latest(run_opts), do: {:ok, [record]} true -> HostKit.RunRecord.list(run_opts) end end defp format_records(records, opts) do case Keyword.get(opts, :format, "text") do "json" -> records |> Enum.map(&JSONCodec.dump/1) |> Jason.encode!(pretty: true) "inspect" -> inspect(records, pretty: true, limit: :infinity) "text" -> Enum.map_join(records, "\n", &format_record(&1, opts)) end end defp format_record(record, opts) do summary = [ record.id, record.direction, record.project, record.applied_at, "changes=#{length(record.changes)}", "artifacts=#{map_size(record.artifacts || %{})}", "backups=#{map_size(record.backups || %{})}" ] |> Enum.reject(&is_nil/1) |> Enum.join(" ") if Keyword.get(opts, :verbose, false) do [ summary, format_paths("artifacts", record.artifacts), format_paths("backups", record.backups) ] |> Enum.reject(&(&1 == "")) |> Enum.join("\n") else summary end end defp format_paths(_label, nil), do: "" defp format_paths(_label, map) when map_size(map) == 0, do: "" defp format_paths(label, map) do lines = map |> Enum.sort_by(fn {key, _path} -> key end) |> Enum.map(fn {key, path} -> " #{label}.#{key}=#{path}" end) Enum.join(lines, "\n") end defp put_present(opts, _key, nil), do: opts defp put_present(opts, key, value), do: Keyword.put(opts, key, value) end