defmodule Mix.Tasks.Verify do @moduledoc """ Runs all checks that CI will enforce, in fast-to-slow order. Stops on the first failure. Designed to be run before every commit to avoid pushing code that will fail CI. ## Steps 1. Elixir formatting (`mix format --check-formatted`) 2. Rust formatting (`cargo fmt --check`) 3. Elixir compilation with warnings as errors 4. Rust clippy with denied warnings 5. Rust unit tests (`cargo test`) 6. Dialyzer type checks 7. Full Elixir test suite (`mix test.seq`) ## Usage mix verify """ use Mix.Task @shortdoc "Run all CI checks locally before committing" @cargo_dir "native/xqlitenif" @steps [ {"Elixir formatting", &__MODULE__.check_elixir_format/0}, {"Rust formatting", &__MODULE__.check_rust_format/0}, {"Elixir compilation (warnings as errors)", &__MODULE__.check_elixir_compile/0}, {"Rust clippy", &__MODULE__.check_rust_clippy/0}, {"Rust tests", &__MODULE__.check_rust_tests/0}, {"Dialyzer", &__MODULE__.check_dialyzer/0}, {"Elixir tests", &__MODULE__.check_tests/0} ] def run(_args) do total = length(@steps) result = @steps |> Enum.with_index(1) |> Enum.reduce_while(:ok, fn {{label, check_fn}, idx}, :ok -> IO.puts("\n[#{idx}/#{total}] #{label}") case check_fn.() do :ok -> IO.puts(" āœ“ passed") {:cont, :ok} {:error, exit_code} -> IO.puts(" āœ— failed (exit code: #{exit_code})") {:halt, {:error, label}} end end) case result do :ok -> IO.puts("\nāœ“ All checks passed. Safe to commit.") {:error, failed_step} -> Mix.raise("verify failed at: #{failed_step}") end end def check_elixir_format do run_cmd("mix", ["format", "--check-formatted"]) end def check_rust_format do run_cargo(["fmt", "--", "--check"]) end def check_elixir_compile do run_cmd("mix", ["compile", "--warnings-as-errors"]) end def check_rust_clippy do run_cargo(["clippy", "--", "-D", "warnings"]) end def check_rust_tests do run_cargo(["test"]) end def check_dialyzer do run_cmd("mix", ["dialyzer"]) end def check_tests do run_cmd("mix", ["test.seq"]) end defp run_cmd(cmd, args) do case System.cmd(cmd, args, into: IO.stream(), stderr_to_stdout: true) do {_, 0} -> :ok {_, exit_code} -> {:error, exit_code} end end # All cargo commands run with cwd set to the crate directory so that the # crate's `.cargo/config.toml` is picked up (e.g., `LIBSQLITE3_FLAGS` for # `SQLITE_ENABLE_STMT_SCANSTATUS`). Cargo walks up from cwd to find the # config; invoking via `--manifest-path` from the repo root does not. defp run_cargo(args) do case System.cmd("cargo", args, cd: @cargo_dir, into: IO.stream(), stderr_to_stdout: true) do {_, 0} -> :ok {_, exit_code} -> {:error, exit_code} end end end