From ae1032695f2f9f72b9758b74c6f8becead689821 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 12:50:35 -0400 Subject: [PATCH] feat: add mix ci task for the complete quality gate Adds Mix.Tasks.Ci which runs the same four steps the CI workflow's test job runs (deps.get, format check, usage_rules.sync check, mix test), all inside app/. CI now calls `mix ci` instead of the individual steps, so local and remote checks are always in sync. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yaml | 17 ++++++---------- lib/mix/tasks/ci.ex | 41 ++++++++++++++++++++++++++++++++++++++ test/mix/tasks/ci_test.exs | 21 +++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 lib/mix/tasks/ci.ex create mode 100644 test/mix/tasks/ci_test.exs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2445487..d03a8eb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,17 +44,12 @@ jobs: app/_build key: ${{ runner.os }}-mix-${{ hashFiles('app/mix.lock') }} - - run: mix deps.get - - - run: mix format --check-formatted - - - # Catches drift introduced within this PR (e.g. an ash/oban bump - # without re-running the sync) - see #79. The reactive auto-fix - # for drift from any other source lives in its own workflow - # (usage-rules-sync.yaml), not here. - run: mix usage_rules.sync --check - - - run: mix test + # mix ci covers deps.get, format --check-formatted, + # usage_rules.sync --check (catches dep-bump drift, see #79), + # and mix test. Runs from the repo root; cd: app/ is handled + # inside the task itself. + run: mix ci + working-directory: . conventional_commits: if: inputs.skip_commit_validation != true diff --git a/lib/mix/tasks/ci.ex b/lib/mix/tasks/ci.ex new file mode 100644 index 0000000..8a81389 --- /dev/null +++ b/lib/mix/tasks/ci.ex @@ -0,0 +1,41 @@ +defmodule Mix.Tasks.Ci do + @shortdoc "Runs the complete quality gate against app/" + + @moduledoc """ + #{@shortdoc}. + + mix ci + + Runs every check `.github/workflows/ci.yaml`'s `test` job runs on a pull + request, in the same order, so a green `mix ci` locally predicts a green + CI run — and CI itself calls this task, so there's one place to fix if + either ever breaks: + + 1. `mix deps.get` — ensure deps are present + 2. `mix format --check-formatted` — code is formatted + 3. `mix usage_rules.sync --check` — usage rules are in sync with deps + (catches drift introduced by a dep bump without re-running the sync; + see #79) + 4. `mix test` — all tests pass + + All steps run inside `app/`. + """ + + use Mix.Task + + alias RepoTasks.Shell + + @impl Mix.Task + def run(argv) do + run(argv, &Shell.run!/3) + end + + @doc false + def run(_argv, shell) do + shell.("mix", ["deps.get"], cd: "app") + shell.("mix", ["format", "--check-formatted"], cd: "app") + shell.("mix", ["usage_rules.sync", "--check"], cd: "app") + shell.("mix", ["test"], cd: "app") + :ok + end +end diff --git a/test/mix/tasks/ci_test.exs b/test/mix/tasks/ci_test.exs new file mode 100644 index 0000000..c6050d4 --- /dev/null +++ b/test/mix/tasks/ci_test.exs @@ -0,0 +1,21 @@ +defmodule Mix.Tasks.CiTest do + use ExUnit.Case, async: true + + alias Mix.Tasks.Ci + + test "runs all quality gate steps in order" do + caller = self() + + shell = fn cmd, args, opts -> + send(caller, {:run, cmd, args, opts}) + :ok + end + + assert :ok = Ci.run([], shell) + + assert_receive {:run, "mix", ["deps.get"], [cd: "app"]} + assert_receive {:run, "mix", ["format", "--check-formatted"], [cd: "app"]} + assert_receive {:run, "mix", ["usage_rules.sync", "--check"], [cd: "app"]} + assert_receive {:run, "mix", ["test"], [cd: "app"]} + end +end