Raxol.Release.PackageCheck (Raxol v2.7.0)

View Source

Validates the Hex release train before publishing any package.

The check is deliberately release-train aware. The repo has package metadata on pre-alpha projects for local builds, but the public Hex train is smaller and ordered by inter-package dependencies.

Three layers run per package:

  • metadata -- project/package/docs keys, link sets, file globs that match something, docs extras that are actually packaged, and dependency requirements that agree with the version each dependency publishes.

  • provenance -- every file the package would ship is tracked by git, so publishing from a dirty tree cannot leak untracked content into a public registry. mix hex.build packages the working tree, not the commit, so a green run on a dirty tree otherwise says nothing about what ships.

  • tarball -- HEX_BUILD=1 mix hex.build --unpack, then the generated hex_metadata.config is compared against the source project config. Comparing against the publish config would be circular: a dependency dropped by an env conditional in mix.exs is absent from both sides and produces no finding at all.

Summary

Functions

Returns every explicitly classified package project.

Compares the dependencies a project declares against the ones it publishes.

Returns the public Hex release train in publish order.

Returns the Raxol package dependencies relevant to a Hex publish.

Resolves the scratch directory the Hex builds unpack into.

Runs the metadata, provenance, and Hex build checks.

Selects packages according to the release checker options.

Lists the files a package would ship that git does not track, relative to root.

Checks that every project under packages/ is classified in this module.

Checks that no package is published before a dependency it needs.

Types

package_report()

@type package_report() :: %{
  app: atom(),
  path: String.t(),
  class: :public | :pre_alpha,
  version: String.t() | nil,
  deps: [{atom(), String.t() | nil, keyword()}],
  hex_build?: boolean(),
  errors: [String.t()],
  warnings: [String.t()]
}

package_spec()

@type package_spec() :: %{app: atom(), path: String.t(), class: :public | :pre_alpha}

report()

@type report() :: %{
  root: String.t(),
  packages: [package_report()],
  global_errors: [String.t()],
  errors: [String.t()]
}

Functions

all_packages()

@spec all_packages() :: [package_spec()]

Returns every explicitly classified package project.

audit_release_deps(source_config, requirement_names)

@spec audit_release_deps(
  keyword(),
  MapSet.t(String.t())
) :: {[String.t()], [String.t()]}

Compares the dependencies a project declares against the ones it publishes.

Audit the source config, never the publish config. Auditing the publish config is circular: a dependency dropped by an env conditional in mix.exs is missing from the publish config and from the tarball, so the two agree and nothing is reported.

A public-train dependency that fails to reach the tarball is an error. A pre-alpha one is an intentional drop, but it still changes what Hex consumers get, so it is surfaced as a warning rather than as silence.

public_packages()

@spec public_packages() :: [package_spec()]

Returns the public Hex release train in publish order.

release_deps(config)

@spec release_deps(keyword()) :: [{atom(), String.t() | nil, keyword()}]

Returns the Raxol package dependencies relevant to a Hex publish.

resolve_build_root(root, opts \\ [])

@spec resolve_build_root(
  String.t(),
  keyword()
) :: {:ok, String.t()} | {:error, [String.t()]}

Resolves the scratch directory the Hex builds unpack into.

build_root is deleted recursively, so it is confined under the project root and may be neither the root itself nor an existing directory this checker did not create. Without those gates run(build_root: ".") deletes the working tree. The path is always interpreted relative to root; an absolute request is jailed under it rather than honoured.

run(opts \\ [])

@spec run(keyword()) :: {:ok, report()} | {:error, report()}

Runs the metadata, provenance, and Hex build checks.

Not safe to run concurrently with anything else in the same VM. Loading a child project goes through Mix.Project.in_project/4, which changes the working directory of the whole OS process, and the HEX_BUILD toggling around it mutates the process environment. Both are global. Tests covering this module are async: false for that reason.

select_packages(opts \\ [])

@spec select_packages(keyword()) :: {:ok, [package_spec()]} | {:error, [String.t()]}

Selects packages according to the release checker options.

:only accepts atoms or strings. Strings are matched against known package names rather than converted, so a typo cannot mint an atom from argv.

untracked_package_files(root, package_path, package)

@spec untracked_package_files(String.t(), String.t(), keyword()) ::
  {:ok, [String.t()]} | :unavailable

Lists the files a package would ship that git does not track, relative to root.

mix hex.build packages the working tree, not the commit, so a green check on a dirty tree says nothing about what a publish actually ships. Returns :unavailable when git cannot answer, which the run reports as a warning rather than treating as a clean result.

validate_catalog(root)

@spec validate_catalog(String.t()) :: [String.t()]

Checks that every project under packages/ is classified in this module.

A package that exists on disk but appears in neither train is unclassified; a classified package with no project on disk is missing. Either one means the release train no longer describes the repo.

validate_order(package_reports, release_train)

@spec validate_order([map()], [package_spec()]) :: [String.t()]

Checks that no package is published before a dependency it needs.

package_reports need only carry :app and :deps; the publish order is the position of each package in release_train.

validate_project_config(spec, config, package_path, release_train \\ [%{app: :raxol_core, class: :public, path: "packages/raxol_core"}, %{app: :raxol_sensor, class: :public, path: "packages/raxol_sensor"}, %{app: :raxol_terminal, class: :public, path: "packages/raxol_terminal"}, %{app: :raxol_mcp, class: :public, path: "packages/raxol_mcp"}, %{app: :raxol_liveview, class: :public, path: "packages/raxol_liveview"}, %{app: :raxol_plugin, class: :public, path: "packages/raxol_plugin"}, %{app: :raxol_speech, class: :public, path: "packages/raxol_speech"}, %{app: :raxol_watch, class: :public, path: "packages/raxol_watch"}, %{app: :raxol, class: :public, path: "."}, %{app: :raxol_agent, class: :public, path: "packages/raxol_agent"}, %{app: :raxol_payments, class: :public, path: "packages/raxol_payments"}, %{app: :raxol_telegram, class: :public, path: "packages/raxol_telegram"}])

@spec validate_project_config(package_spec(), keyword(), String.t(), [package_spec()]) ::
  {[String.t()], [String.t()]}

Validates one loaded Mix project config against a package spec.