lib/git_hub_actions.ex

defmodule GitHubActions do
  @moduledoc """
  A little tool to write GitHub actions in Elixir.
  """

  import Mix.Generator

  alias GitHubActions.Config
  alias GitHubActions.Workflow
  alias GitHubActions.Yaml

  @version Mix.Project.config()[:version]

  @workflow "default.exs"
  @config "config.exs"
  @dir ".gha"

  @doc """
  Creates a GitHub actions workflow file.
  """
  @spec run(keyword()) :: boolean()
  def run(opts) do
    read_config(opts)

    opts
    |> workflow()
    |> Yaml.encode()
    |> write(opts)
  end

  @doc false
  def copy(gen_global: true) do
    create_directory(global())
    copy_file(priv(@config), global(@config))
    copy_file(priv(@workflow), global(@workflow))
  end

  def copy(gen_local: true) do
    create_directory(local())
    copy_file(priv(@config), local(@config))
    copy_file(priv(@workflow), local(@workflow))
  end

  def copy(_opts) do
    raise GitHubActions.Error, "option --gen-local or --gen-global is required"
  end

  defp read_config(opts) do
    # read default config first
    Config.read(priv(@config))
    if File.exists?(global(@config)), do: Config.read(global(@config))
    if File.exists?(local(@config)), do: Config.read(local(@config))

    case Keyword.fetch!(opts, :config) do
      :default -> :ok
      config -> Config.read(config)
    end
  end

  defp workflow do
    case default_input() do
      {:ok, path} ->
        workflow(path)

      {:error, script} ->
        raise GitHubActions.Error, "no workflow script found, sought: #{inspect(script)}"
    end
  end

  defp workflow(opts) when is_list(opts) do
    opts |> Keyword.fetch!(:workflow) |> workflow()
  end

  defp workflow(:default), do: workflow()

  defp workflow(path) do
    case Workflow.eval(path) do
      {:ok, workflow} -> workflow
      :error -> raise GitHubActions.Error, "invalid workflow script, script: #{inspect(path)}"
    end
  end

  defp default_input do
    workflow = Config.get([:input, :default], @workflow)

    Enum.find_value(
      [
        local(workflow),
        global(workflow),
        priv(workflow)
      ],
      {:error, workflow},
      fn path ->
        with true <- File.exists?(path) do
          {:ok, path}
        end
      end
    )
  end

  defp write(yaml, opts) do
    path = output_path(opts)

    comment =
      case Config.get([:output, :comment], true) do
        true -> "# Created with GitHubActions version #{@version}\n"
        false -> ""
      end

    create_file(
      path,
      comment <> yaml,
      force: true
    )
  end

  defp output_path(opts) do
    with :default <- Keyword.fetch!(opts, :output) do
      Path.join(
        Config.fetch!([:output, :path]),
        Config.fetch!([:output, :file])
      )
    end
  end

  defp local, do: @dir

  defp global, do: Path.join(System.user_home!(), @dir)

  defp local(file), do: Path.join(@dir, file)

  defp global(file), do: Path.join([System.user_home!(), @dir, file])

  defp priv(file), do: Path.join(:code.priv_dir(:git_hub_actions), file)

  defmodule Error do
    defexception [:message]
  end
end