diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 971c6fd668..7372fd62bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: - name: Run installer test run: | cd installer + mix deps.get mix test if: ${{ matrix.installer }} diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex index ad7958ee67..c50ddb64d2 100644 --- a/installer/lib/mix/tasks/phx.new.ex +++ b/installer/lib/mix/tasks/phx.new.ex @@ -69,6 +69,8 @@ defmodule Mix.Tasks.Phx.New do * `-v`, `--version` - prints the Phoenix installer version + * `--no-version-check` - skip the version check for the latest phx_new version + When passing the `--no-ecto` flag, Phoenix generators such as `phx.gen.html`, `phx.gen.json`, `phx.gen.live`, and `phx.gen.context` may no longer work as expected as they generate context files that rely @@ -142,7 +144,8 @@ defmodule Mix.Tasks.Phx.New do mailer: :boolean, adapter: :string, inside_docker_env: :boolean, - from_elixir_install: :boolean + from_elixir_install: :boolean, + version_check: :boolean ] @reserved_app_names ~w(server table) @@ -155,17 +158,40 @@ defmodule Mix.Tasks.Phx.New do def run(argv) do elixir_version_check!() - case OptionParser.parse!(argv, strict: @switches) do - {_opts, []} -> - Mix.Tasks.Help.run(["phx.new"]) + {opts, argv} = OptionParser.parse!(argv, strict: @switches) - {opts, [base_path | _]} -> - if opts[:umbrella] do - generate(base_path, Umbrella, :project_path, opts) - else - generate(base_path, Single, :base_path, opts) - end + version_task = + if Keyword.get(opts, :version_check, true) do + get_latest_version("phx_new") + end + + result = + case {opts, argv} do + {_opts, []} -> + Mix.Tasks.Help.run(["phx.new"]) + + {opts, [base_path | _]} -> + if opts[:umbrella] do + generate(base_path, Umbrella, :project_path, opts) + else + generate(base_path, Single, :base_path, opts) + end + end + + if version_task do + try do + # if we get anything else than a `Version`, we'll get a MatchError + # and fail silently + %Version{} = latest_version = Task.await(version_task, 3_000) + maybe_warn_outdated(latest_version) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end end + + result end @doc false @@ -404,4 +430,88 @@ defmodule Mix.Tasks.Phx.New do ) end end + + defp maybe_warn_outdated(latest_version) do + if Version.compare(@version, latest_version) == :lt do + Mix.shell().info([ + :yellow, + "A new version of phx.new is available:", + :green, + " v#{latest_version}", + :reset, + ".", + "\n", + "You are currently running ", + :red, + "v#{@version}", + :reset, + ".\n", + "To update, run:\n\n", + " $ mix local.phx\n" + ]) + end + end + + # we need to parse JSON, so we only check for new versions on Elixir 1.18+ + if Version.match?(System.version(), "~> 1.18") do + defp get_latest_version(package) do + Task.async(fn -> + # ignore any errors to not prevent the generators from running + # due to any issues while checking the version + try do + with {:ok, package} <- get_package(package) do + versions = + for release <- package["releases"], + version = Version.parse!(release["version"]), + # ignore pre-releases like release candidates, etc. + version.pre == [] do + version + end + + Enum.max(versions, Version) + end + rescue + e -> {:error, e} + catch + :exit, _ -> {:error, :exit} + end + end) + end + + defp get_package(name) do + http_options = + [ + ssl: [ + verify: :verify_peer, + cacerts: :public_key.cacerts_get(), + depth: 2, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ], + versions: [:"tlsv1.2", :"tlsv1.3"] + ] + ] + + options = [body_format: :binary] + + case :httpc.request( + :get, + {~c"https://hex.pm/api/packages/#{name}", + [{~c"user-agent", ~c"Mix.Tasks.Phx.New/#{@version}"}]}, + http_options, + options + ) do + {:ok, {{_, 200, _}, _headers, body}} -> + {:ok, JSON.decode!(body)} + + {:ok, {{_, status, _}, _, _}} -> + {:error, status} + + {:error, reason} -> + {:error, reason} + end + end + else + defp get_latest_version(_), do: nil + end end diff --git a/installer/mix.lock b/installer/mix.lock index 394ffc7366..ae885bb82a 100644 --- a/installer/mix.lock +++ b/installer/mix.lock @@ -1,5 +1,4 @@ %{ - "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, diff --git a/integration_test/test.sh b/integration_test/test.sh index a2cdb03528..ccddbf89e6 100755 --- a/integration_test/test.sh +++ b/integration_test/test.sh @@ -14,6 +14,7 @@ socat TCP-LISTEN:1433,fork TCP-CONNECT:mssql:1433& # Run installer tests echo "Running installer tests" cd installer +mix deps.get mix test echo "Running integration tests"