defmodule GitHubActions.Default do use GitHubActions.Workflow def workflow do [ name: "CI", env: [ GITHUB_TOKEN: ~e[secrets.GITHUB_TOKEN] ], on: ~w(pull_request push), jobs: [ linux: job(:linux), windows: job(:windows), macos: job(:macos) ] ] end defp elixir_version do elixir = Config.get(:elixir) if elixir, do: "~> #{Version.minor(elixir)}", else: Project.elixir() end defp otp_version, do: "> 24.0.0" defp matrix do [ elixir: elixir_version(), otp: otp_version(), mode: :include ] |> Versions.matrix() |> Keyword.update!(:include, &Versions.minimize/1) |> Keyword.update!(:include, &add_at(&1, 0, coverage: true, lint: true)) end defp add_at(list, pos, keywords) do List.update_at(list, pos, &Enum.concat(&1, keywords)) end defp job(:linux = os) do job(os, name: """ Test on #{Config.get([os, :name])} (\ Elixir #{~e[matrix.elixir]}, \ OTP #{~e[matrix.otp]})\ """, runs_on: Config.get([os, :runs_on]), strategy: [matrix: matrix()], steps: [ checkout(), setup_elixir(os), restore(:deps), restore(:_build), restore(:dialyxir), get_deps(), compile_deps(os), compile(os), check_unused_deps(), check_code_format(), lint_code(), run_tests(os), run_coverage(os), dialyxir() ] ) end defp job(:windows = os) do job(os, name: "Test on #{Config.get([os, :name])}", runs_on: Config.get([os, :runs_on]), steps: [ checkout(), setup_elixir(os), restore(:deps, key: [matrix: false]), restore(:_build, key: [matrix: false]), get_deps(), compile_deps(os), compile(os), run_tests(os), run_coverage(os) ] ) end defp job(:macos = os) do job(os, name: "Test on #{Config.get([os, :name])}", runs_on: Config.get([os, :runs_on]), steps: [ checkout(), setup_elixir(os), install_hex(), install_rebar(), restore(:deps, key: [matrix: false]), restore(:_build, key: [matrix: false]), get_deps(), compile_deps(os), compile(os), run_tests(os), run_coverage(os) ] ) end defp job(os, config) do case member?(:jobs, os) do true -> config false -> :skip end end defp checkout do [ name: "Checkout", uses: "actions/checkout@v4" ] end defp setup_elixir(:linux) do [ name: "Setup Elixir", id: "setup-beam", uses: "erlef/setup-beam@v1", with: [ elixir_version: ~e[matrix.elixir], otp_version: ~e[matrix.otp] ] ] end defp setup_elixir(os) when os in [:macos, :windows] do [ name: "Setup Elixir", id: "setup-beam", uses: "erlef/setup-beam@v1", with: [ elixir_version: Versions.latest(:elixir), otp_version: Versions.latest(:otp) ] ] end defp install_hex do [ name: "Install hex", run: mix(:local, :hex, force: true) ] end defp install_rebar do [ name: "Install rebar", run: mix(:local, :rebar, force: true) ] end defp restore(path, opts \\ []) defp restore(:dialyxir, opts) do case Project.has_dep?(:dialyxir) and Config.get([:steps, :dialyxir]) do false -> :skip true -> case Project.fetch([:dialyzer, :plt_file]) do :error -> :skip {:ok, {_, file}} -> file |> Path.dirname() |> restore([if: ~e[matrix.lint]] ++ opts) end end end defp restore(path, opts) do {key_opts, opts} = Keyword.pop(opts, :key, []) case Config.fetch!([:steps, :refresh]) do false -> :skip true -> {opt_if, opts} = Keyword.pop(opts, :if) opt_if = if opt_if, do: [if: opt_if], else: [] Keyword.merge( [name: "Restore #{path}"] ++ opt_if ++ [ uses: "actions/cache@v4", with: [ path: path, key: key(path, key_opts) ] ], opts ) end end defp get_deps do [ name: "Get dependencies", run: mix(:deps, :get) ] end defp compile_deps(os) do [ name: "Compile dependencies", run: mix(:deps, :compile, env: :test, os: os) ] end defp compile(os) do [ name: "Compile project", run: mix(:compile, warnings_as_errors: true, env: :test, os: os) ] end defp check_unused_deps do case Config.get(:check_unused_deps, true) do false -> :skip true -> [ name: "Check unused dependencies", if: ~e[matrix.lint], run: mix(:deps, :unlock, check_unused: true) ] end end defp check_code_format do case Config.get(:check_code_format, true) do false -> :skip true -> [ name: "Check code format", if: ~e[matrix.lint], run: mix(:format, check_formatted: true) ] end end defp lint_code do case Project.has_dep?(:credo) do false -> :skip true -> [ name: "Lint code", if: ~e[matrix.lint], run: mix(:credo, strict: true) ] end end defp run_tests(os) when os in [:windows, :macos] do [ name: "Run tests", run: mix(:test) ] end defp run_tests(_nix) do case Project.has_dep?(:excoveralls) do true -> [ name: "Run tests", if: ~e[!matrix.coverage], run: mix(:test) ] false -> [ name: "Run tests", run: mix(:test) ] end end defp run_coverage(os) when os in [:windows, :macos], do: :skip defp run_coverage(_nix) do case Project.has_dep?(:excoveralls) do true -> [ name: "Run tests with coverage", if: ~e[matrix.coverage], run: mix(:coveralls, Config.get([:steps, :coveralls])) ] false -> :skip end end defp dialyxir do case Project.has_dep?(:dialyxir) do false -> :skip true -> [ name: "Static code analysis", if: ~e[matrix.lint], run: mix(:dialyzer, force_check: true, format: "github") ] end end defp key(key, opts) do matrix? = Keyword.get(opts, :matrix, true) os = ~e[runner.os] setup_beam_version = ~e{steps.setup-beam.outputs.setup-beam-version} lock = ~e[hashFiles(format('{0}{1}', github.workspace, '/mix.lock'))] if matrix? do elixir = ~e[matrix.elixir] otp = ~e[matrix.otp] ~q"#{key}\n-#{os}\n-#{elixir}\n-#{otp}\n-#{lock}\n-#{setup_beam_version}"e else ~q"#{key}\n-#{os}\n-#{lock}\n-#{setup_beam_version}"e end end defp member?(key, value), do: key |> Config.get() |> Enum.member?(value) end