defmodule Mix.Tasks.Dexcord.Coverage.Refresh do @shortdoc "Downloads and pins the latest Discord OpenAPI spec into priv/" @moduledoc """ Refreshes `priv/discord_openapi.json` from the upstream `discord/discord-api-spec` repo. This is a DELIBERATE, human-initiated action — it is never run in CI. Pinning means upstream churn can only affect coverage when an operator explicitly runs this task, reviews the `git diff`, reconciles the allowlist, and reruns `mix dexcord.coverage`. """ use Mix.Task @url "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json" @impl Mix.Task def run(_argv) do # Mix prunes OTP apps it doesn't depend on from the code path, so inets/ssl # (needed only by this deliberate, dev-only task) may not be loadable. Add # their ebin dirs from the Erlang root before starting them. ensure_otp_app_on_path(:ssl) ensure_otp_app_on_path(:inets) {:ok, _} = Application.ensure_all_started(:ssl) {:ok, _} = Application.ensure_all_started(:inets) dest = Path.join(:code.priv_dir(:dexcord), "discord_openapi.json") File.mkdir_p!(Path.dirname(dest)) Mix.shell().info("fetching #{@url} ...") body = fetch!(@url) File.write!(dest, body) Mix.shell().info("wrote #{dest} (#{byte_size(body)} bytes)") Mix.shell().info( "review `git diff priv/discord_openapi.json`, reconcile priv/coverage_allowlist.exs, then rerun `mix dexcord.coverage`" ) end # Adds an OTP application's ebin dir (under the Erlang root) to the code path # if its modules aren't already loadable. defp ensure_otp_app_on_path(app) do if :code.lib_dir(app) == {:error, :bad_name} do root = to_string(:code.root_dir()) case Path.wildcard(Path.join([root, "lib", "#{app}-*", "ebin"])) do [ebin | _] -> :code.add_patha(String.to_charlist(ebin)) [] -> Mix.raise("cannot locate OTP application #{app} under #{root}/lib") end end end # Verifies the peer with castore's CA bundle so the pin can't be MITM'd. defp fetch!(url) do cacerts = CAStore.file_path() |> to_charlist() http_opts = [ ssl: [ verify: :verify_peer, cacertfile: cacerts, depth: 3, customize_hostname_check: [ match_fun: :public_key.pkix_verify_hostname_match_fun(:https) ] ], autoredirect: true ] case :httpc.request(:get, {to_charlist(url), []}, http_opts, body_format: :binary) do {:ok, {{_v, 200, _r}, _headers, body}} -> body {:ok, {{_v, status, _r}, _headers, _body}} -> Mix.raise("failed to fetch spec: HTTP #{status}") {:error, reason} -> Mix.raise("failed to fetch spec: #{inspect(reason)}") end end end