From 324fa2632cdbbc20761ea1fe1a4c3d4d55b89e52 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 28 Jun 2026 23:47:37 -0700 Subject: [PATCH] feat: list package overrides in mix hex.policy show List each package override (package, requirement, ALLOW/DENY) in the per-repository output of mix hex.policy show instead of only printing a count, and render a (none) placeholder when a repository has no overrides. Drop the implicit show default so a bare mix hex.policy now raises the usage message instead of silently running show. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MQ7G74DcZPFPEgDJMTEcCR --- lib/mix/tasks/hex.policy.ex | 40 ++++++++++++++++++++----- test/mix/tasks/hex.policy_test.exs | 47 ++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/lib/mix/tasks/hex.policy.ex b/lib/mix/tasks/hex.policy.ex index f8c00dc9..d6bca415 100644 --- a/lib/mix/tasks/hex.policy.ex +++ b/lib/mix/tasks/hex.policy.ex @@ -9,7 +9,6 @@ defmodule Mix.Tasks.Hex.Policy do Shows the active Hex policy and explains why specific versions are blocked. - $ mix hex.policy $ mix hex.policy show $ mix hex.policy why PACKAGE @@ -17,8 +16,7 @@ defmodule Mix.Tasks.Hex.Policy do * `show` — Summarize the active policy: visibility and the per-repository restrictions (cooldown, advisory, retirement) and - override counts, plus the effective cooldown. This is the default - when no command is given. + the package overrides, plus the effective cooldown. * `why PACKAGE` — Walk every version of the named package in the registry, classify each against the active policy, and print a per-version table of status and the reasons it is blocked. @@ -55,9 +53,6 @@ defmodule Mix.Tasks.Hex.Policy do Hex.start() case args do - [] -> - show() - ["show"] -> show() @@ -137,9 +132,40 @@ defmodule Mix.Tasks.Hex.Policy do Hex.Shell.info([" Cooldown: ", cooldown_label(restriction)]) Hex.Shell.info([" Advisory rule: ", advisory_label(restriction)]) Hex.Shell.info([" Retirement rule: ", retirement_label(restriction)]) - Hex.Shell.info([" Overrides: ", to_string(length(overrides))]) + render_overrides(overrides) + end + + defp render_overrides([]) do + Hex.Shell.info([" Overrides: ", [:faint, "(none)"]]) + end + + defp render_overrides(overrides) do + Hex.Shell.info([" Overrides:"]) + + Enum.each(overrides, fn override -> + Hex.Shell.info([" ", override_label(override)]) + end) end + defp override_label(%{action: action, ref: ref}) do + package = Map.get(ref, :package) + + package_text = + case Map.get(ref, :requirement) do + requirement when is_binary(requirement) and requirement != "" -> + "#{package} #{requirement}" + + _ -> + "#{package}" + end + + [package_text, " ", override_action_label(action)] + end + + defp override_action_label(:OVERRIDE_ACTION_ALLOW), do: [:green, "ALLOW"] + defp override_action_label(:OVERRIDE_ACTION_DENY), do: [:red, "DENY"] + defp override_action_label(other), do: [to_string(other)] + defp why(arg) do {repo, package} = parse_package_arg(arg) diff --git a/test/mix/tasks/hex.policy_test.exs b/test/mix/tasks/hex.policy_test.exs index b13e29fd..693b509b 100644 --- a/test/mix/tasks/hex.policy_test.exs +++ b/test/mix/tasks/hex.policy_test.exs @@ -41,13 +41,54 @@ defmodule Mix.Tasks.Hex.PolicyTest do assert out =~ "HIGH" end - test "a bare invocation (no subcommand) defaults to show" do - out = capture_io(fn -> Mix.Tasks.Hex.Policy.run([]) end) - assert out =~ "No active policy" + test "lists each package override with its action" do + Hex.State.put(:active_policy, %{ + repository: "myorg", + name: "strict-prod", + visibility: :VISIBILITY_PUBLIC, + repositories: [ + %{ + repository: "hexpm", + restriction: %{}, + overrides: [ + %{action: :OVERRIDE_ACTION_ALLOW, ref: %{package: "plug", requirement: ">= 1.0.0"}}, + %{action: :OVERRIDE_ACTION_DENY, ref: %{package: "evil_dep"}} + ] + } + ] + }) + + out = capture_io(fn -> Mix.Tasks.Hex.Policy.run(["show"]) end) + assert out =~ "plug" + assert out =~ ">= 1.0.0" + assert out =~ "ALLOW" + assert out =~ "evil_dep" + assert out =~ "DENY" + end + + test "renders a (none) placeholder when a repository has no overrides" do + Hex.State.put(:active_policy, %{ + repository: "myorg", + name: "strict-prod", + visibility: :VISIBILITY_PUBLIC, + repositories: [ + %{repository: "hexpm", restriction: %{}, overrides: []} + ] + }) + + out = capture_io(fn -> Mix.Tasks.Hex.Policy.run(["show"]) end) + assert out =~ "Overrides:" + assert out =~ "(none)" end end describe "why" do + test "a bare invocation (no subcommand) raises the usage message" do + assert_raise Mix.Error, ~r/Invalid arguments, expected one of:/, fn -> + Mix.Tasks.Hex.Policy.run([]) + end + end + test "complains when package name is missing" do assert_raise Mix.Error, ~r/Invalid arguments, expected one of:/, fn -> Mix.Tasks.Hex.Policy.run(["why"])