Dependency policies#1168
Draft
ericmj wants to merge 25 commits into
Draft
Conversation
Adds mix_hex_pb_policy generated module and extends the vendoring script to track hex_pb_policy. mix_hex_registry gains encode_policy/ decode_policy/build_policy/unpack_policy and mix_hex_repo gains get_policy/2, mirroring the recently-added hex_core API.
Adds the :policy slot in Hex.State (env HEX_POLICY, config :policy, default []). Hex.Policy.Sources parses the three accepted shapes — keyword list, list of keyword lists, comma-separated string — and deduplicates by (repo, name) preserving order. Hex.Repo gains get_policy/2 wrapping the vendored :mix_hex_repo.get_policy/2.
Parallel HTTP fetch via Hex.Repo.get_policy, signature-verified through the vendored mix_hex_repo. Decoded policies are stored on disk as erlang terms with an ETag sidecar; on fetch failure the last-known-good copy is loaded with a stale warning. Caches older than 30 days hard-fail for that specific policy with a clear error. Hex.Repo.get_policy/2 now derives repo_organization from the "hexpm:<org>" key and strips the /repos/<org> URL suffix so the vendored hex_repo can build the right policy URL and verify the payload's repository field. Adds two computed Hex.State slots, :policies and :policy_filtered_versions, as stash points for later phases.
Hex.Policy.Filter.classify/3 evaluates a single policy against a
release (advisory severity threshold + retirement reason set);
classify_set/3 ANDs across the active set and records every blocker
for diagnostics.
Hex.Policy.Cooldown.strictest/2 combines local and policy cooldown
contributions and returns the longest duration string. source/2
identifies the contributor (`:local` or `{repo, name}`).
Hex.Policy.load_all reads the configured refs, dedups, and fetches the active set with cache fallback. converge/2 stashes the result under Hex.State up-front; on load failure Mix.raise surfaces a clear error. setup_cooldown now combines the local cooldown with policy cooldowns via Hex.Policy.Cooldown.strictest/2 — the existing cooldown filter handles age-based rejection for both sources. The solver runs against Hex.Registry.Policy, which wraps Hex.Registry.Cooldown: per-candidate classification via Hex.Policy.Filter.classify_set/2, with denied candidates filtered out and diagnostics recorded under :policy_filtered_versions.
Hex.Policy.Diagnostics owns rendering: resolution_summary/3, preflight_error/4, failure_note/1, and format_load_error/1. RemoteConverger.policy_preflight! runs after verify_input and before the solver, scanning every direct request whose constraint has no eligible version under the active policy set and raising a focused preflight_error. After every resolution that loaded a policy, print_policy_summary prints the active set, the effective cooldown, and per-policy hidden counts. On solver failure, a Note: block is appended listing the responsible policies for transitive deps whose candidate set was emptied. normalize_advisories / severity_atom_to_int were lifted from Hex.Registry.Policy into Hex.Policy.Filter so both the registry wrapper and the pre-flight share one canonical conversion.
`mix hex.policy show` (default) prints the active policy set with per-policy visibility, cooldown, advisory rule, retirement rule, and the effective cooldown. `mix hex.policy why PACKAGE` walks every version of PACKAGE in the registry, classifies each against the active policies via Hex.Policy.Filter, and prints a per-version status table.
`mix hex.policy why` now loads policies cold (same fallback as `show`) and accepts a `REPO/PACKAGE` form so private-repo packages can be inspected. Per-version blocker output names the reason (advisory severity / retirement reason) instead of collapsing to just the policy name. Loader.read_cache tolerates a missing .etag sidecar instead of crashing. Diagnostics.preflight_error no longer hardcodes a `** (Mix) ` prefix — that's Mix.raise/1's job — so the rendered error doesn't double up.
`mix help hex.policy` now shows the three opt-in mechanisms (mix.exs, HEX_POLICY, mix hex.config) with examples, and points at the new hex.pm docs page. The `mix hex.config` `policy` bullet clarifies the CLI form (string) versus the mix.exs richer keyword form, so users don't try to pass keyword lists through `mix hex.config KEY VALUE`.
Sources.load_all/0 reads policies from mix.exs, HEX_POLICY, and ~/.hex/hex.config independently and unions+dedups. The :policy state slot is removed — Hex.Policy.Sources is now the source of truth, matching the documented intersection-across-sources behavior. Tests cover multi-source union explicitly.
The 304-conditional branch was unreachable — Hex.Repo.get_policy/2 never threaded an etag through build_hex_core_config/3 — and the .etag sidecar was write-only. Removing the etag plumbing simplifies the loader and tightens the cache state machine.
The 30-day staleness cap is about how long a network adversary can suppress refreshes — cache file age, not policy publish time. Switch days_old/1 to read File.stat mtime. The test now touches the cached file backwards in time to exercise the stale-cap path.
Reject inputs like "myorg/" and "/foo" that produce empty repo or package halves; surface a Mix.raise instead of carrying empty strings into the registry.
The global hexpm repo carries no organization-scoped policies and Hex.Repo.get_policy/2 would have nothing useful to do with a bare "hexpm" ref. Reject "hexpm/<name>" in both string and keyword configurations so the failure surfaces at config-parse time.
Picks up the upstream removal of Policy.published_at and the contiguous renumbering (visibility=4, advisory_min_severity=5, retirement_reasons=6, cooldown=7). Drops a now-meaningless comment in the loader.
Resolution can succeed even when individual versions are blocked by an active policy, so failing before the solver runs is wrong. Let the solver explore the candidate set; Hex.Registry.Policy filters at solve time and Diagnostics.failure_note attributes blocks on failure.
Drop the bespoke Hex.Policy.Loader and route policy fetches through the same ETS-backed cache, Hex.Parallel runner, and etag/304 path that handles /packages/<name>. Adds prefetch_policies/1 + policy/2 to Hex.Registry.Server, get_policy/3 etag arg to Hex.Repo, and honors HEX_OFFLINE.
…icy.Filter The filter carried two parallel representations of every enum: hex_core decoded atoms from the registry, plus an int form invented to match the threshold passed in by the policy resource. Converting the threshold to its atom symbol once and ranking with Enum.find_index removes the severity_to_int / retired_to_int / normalize_advisories helpers and lets blockers describe themselves with the symbol the rest of the codebase already speaks.
Hex.Policy.Cooldown duplicated duration parsing and source attribution that already lived in Hex.Cooldown, and the remote converger held a second copy of the bypass/locked-versions builders. Collapsing both into Hex.Cooldown — strictest/1 over a tagged list, setup/2 as the single entry point — drops a module and the converger's private helpers.
The diagnostics module shipped its own severity_label/retirement_label clauses keyed on the int form that Hex.Policy.Filter no longer carries. Delegating to Hex.Utils.advisory_severity/1 and package_retirement_reason/1 keeps blocker output consistent with mix hex.audit and removes the parallel label table.
The hex.policy task carried its own active_policies_or_load helper that read Hex.State, fell back to Hex.Policy.load_all, and cached the result. That belongs next to the loader itself, so callers that need the active set ask Hex.Policy for it directly instead of reimplementing the cache.
The task duplicated severity/retirement label tables that already live in Hex.Utils, and rendered why output by ad-hoc string concatenation. Going through the shared helpers and Mix.Tasks.Hex.print_table aligns hex.policy with the rest of the CLI surface (matching hex.audit's uppercase severity labels).
Hex.Policy.Sources ran its own project/env/global config plumbing in parallel to Hex.State. Splitting the policy key into project / env / global entries and adding a config_scope flag lets Hex.State own parsing and source attribution, so Sources.load_all/0 is now just a union over three state lookups.
The function carried its own \"hexpm:\" <> org parser and a suffix-strip of the cached URL to recover the organization. default_organization/3 already builds the config map with everything else derived from the parent repo, so stashing the organization name there at construction time turns get_policy/3 into a simple Map.get lookup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.