Current section

Files

Jump to
Raw

mix.exs

defmodule MailglassAdmin.MixProject do
  use Mix.Project

  @version "2.7.0"
  @source_url "https://github.com/szTheory/mailglass"
  @description "Mountable LiveView dashboard for mailglass — dev preview + admin"

  def project do
    [
      app: :mailglass_admin,
      version: @version,
      elixir: "~> 1.18",
      elixirc_paths: elixirc_paths(Mix.env()),
      elixirc_options: elixirc_options(),
      compilers: [:boundary | Mix.compilers()],
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases(),
      dialyzer: dialyzer(),
      test_coverage: [tool: ExCoveralls],
      name: "MailglassAdmin",
      description: @description,
      source_url: @source_url,
      homepage_url: @source_url,
      package: package(),
      docs: docs()
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end

  def cli do
    [
      preferred_envs: [
        ci: :test,
        "ci.fast": :test,
        "mailglass_admin.preview.capture": :test,
        "verify.preview": :test,
        "verify.phase_05": :test,
        "verify.support_contract.admin": :test
      ]
    ]
  end

  defp dialyzer do
    [
      # D-08-01: :no_opaque + :no_match kill the Elixir 1.18 opaque-type
      # cascade (elixir-lang/elixir#14837). :error_handling, :missing_return,
      # :underspecs improve coverage; revisit removing :underspecs after the
      # <=15-entry baseline is hit (D-08-07).
      flags: [:error_handling, :missing_return, :no_opaque, :no_match, :underspecs],
      # D-08-03: ignore_file_strict pins to {file, short_description} tuples
      # (stable across line-number drift). D-08-04: list_unused_filters fails
      # CI loudly when a future fix invalidates an existing ignore (prevents
      # silent drift).
      ignore_file_strict: ".dialyzer_ignore.exs",
      list_unused_filters: true,
      # Add :credo and :mix to PLT so dev-only modules don't produce
      # callback_info_missing + unknown_function warnings. credo is a :dev/:test
      # dep; without :credo in the PLT, the 13 custom check files each generate
      # ~11 dialyzer warnings. Mix.Task behaviour is similarly dev-only.
      plt_add_apps: [:credo, :mix, :ex_unit]
    ]
  end

  # "dev/" holds internal CI-only preview-capture tooling (Chromium screenshot
  # driver + mix mailglass_admin.preview.capture). Compiled in :dev and :test for
  # CI/local use, but kept out of "lib" so it never ships in the Hex package
  # (mix.exs :package :files lists "lib", not "dev").
  defp elixirc_paths(:dev), do: ["lib", "dev"]

  # RATCHET-01 / CONTEXT D-06 mechanism 2 (A1 fallback — Pitfall 3): the
  # canonical persona cohort spec lives in the demo app, but a `path:` dep on
  # the whole `mailglass_demo` app is structurally impossible — the demo
  # depends on `mailglass_admin`, so a back-dep is a circular path dep ("another
  # project with the same name was already defined"). Instead we compile the
  # single canonical spec directory directly into the admin TEST build.
  # `reference/persona_spec/personas.ex` is a pure module — it references only
  # core schemas (`Mailglass.Outbound.Delivery`, `Mailglass.Events.Event`), no
  # demo-app or Phoenix deps — so it compiles cleanly here. This keeps the spec
  # a single source of truth (admin test-support reads
  # `MailglassDemo.Personas.spec/0`) without crossing into prod or the
  # `--no-optional-deps` lane (it is `:test`-only, just like `test/support`).
  @persona_spec_dir Path.expand("../reference/persona_spec", __DIR__)
  defp elixirc_paths(:test), do: ["lib", "test/support", "dev", @persona_spec_dir]
  defp elixirc_paths(_), do: ["lib"]

  # Optional modules are referenced only behind runtime load checks. Declaring
  # them here keeps the no-optional-deps CI lane warning-clean when their
  # packages are genuinely absent.
  defp elixirc_options do
    [no_warn_undefined: [Phoenix.LiveReloader, MailglassAdmin.OptionalDeps.MailglassInbound]]
  end

  defp deps do
    [
      # Local-dev: path dep so changes in ../lib/mailglass/ are picked up
      # immediately. Published Hex tarball: pinned version match (linked
      # versions per CONTEXT D-02 / DIST-01 / PREV-01).
      mailglass_dep(),
      # Optional sibling: the admin LiveView reads inbound rows through the
      # `MailglassAdmin.OptionalDeps.MailglassInbound` runtime gateway. The dep
      # is FLOATING (`~> 0.2`, never `==`) and `optional: true` so the
      # `mix compile --no-optional-deps --warnings-as-errors` lane compiles with
      # inbound stripped (CONTEXT D-48-01, Pitfall 4). It is INTENTIONALLY OUT of
      # the release-please PINS array — a `==` cross-line pin into a 0.2.x package
      # is unsatisfiable; the gateway, not a version pin, is the integration seam.
      mailglass_inbound_dep(),
      {:phoenix, "~> 1.8"},
      {:phoenix_live_view, "~> 1.1"},
      {:phoenix_html, "~> 4.1"},
      {:plug, "~> 1.18"},
      {:nimble_options, "~> 1.1"},
      # Build tooling (CONTEXT D-18). No :esbuild at v0.1 (pure LiveView, no custom JS).
      {:tailwind, "~> 0.4", only: [:dev, :test], runtime: false},
      # Optional dev dep (CONTEXT D-24). Adopter-owned LiveReload subscription.
      # `:only [:dev, :test]` so preview_live_test.exs can exercise the
      # LiveReload subscribe + broadcast path; the dep remains `optional: true`
      # so adopters can omit it entirely in prod-admin (v0.5) configurations.
      {:phoenix_live_reload, "~> 1.6", optional: true, only: [:dev, :test]},
      {:plug_cowboy, "~> 2.7", only: :test},
      {:boundary, "~> 0.10", runtime: false},
      # floki + jason: unrestricted :only scope because the mailglass core
      # path dep uses them at runtime. Mix rejects divergent :only options
      # on shared transitive deps (Rule 3 blocker resolution).
      {:floki, "~> 0.38"},
      {:jason, "~> 1.4"},
      # Dev/test
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:ex_doc, "~> 0.40", only: :dev, runtime: false},
      # Phoenix.LiveViewTest 1.1+ requires lazy_html for DOM traversal
      # (replaces the previous floki-based implementation).
      {:lazy_html, ">= 0.1.0", only: :test},
      # GREEN-02: measured coverage-floor ratchet, mirroring core and inbound
      # (mailglass_inbound/mix.exs, same ~> 0.18 pin already locked repo-wide).
      {:excoveralls, "~> 0.18", only: [:test]}
    ]
  end

  # CONTEXT D-02 linked-versions switch (the ONE pattern with no analog in
  # mailglass core's mix.exs): local-dev uses a path dep so the sibling
  # packages evolve together; publishing to Hex uses a pessimistic `~>` constraint
  # (v1.15 Phase 125, LD-2). Admin is in the linked-versions group
  # [mailglass, mailglass_admin] — the Release Please plugin release-time-locks
  # admin's minor to core, so `~> 2.0` is safe: admin never resolves against a
  # core minor it was not shipped with. A new core minor requires a linked admin
  # release, which updates this line.
  defp mailglass_dep do
    if System.get_env("MIX_PUBLISH") == "true" do
      {:mailglass, "~> 2.0"}
    else
      {:mailglass, path: "..", override: true}
    end
  end

  # CONTEXT D-48-01: the optional inbound sibling. Mirrors `mailglass_dep/0`'s
  # MIX_PUBLISH branch STRUCTURE (path dep for local-dev, version constraint when
  # publishing) but is FLOATING (`~> 2.0`, NEVER `== X.Y.Z`) and `optional: true`.
  #
  # Why floating, not pinned: `mailglass_inbound` tracks its own version line and
  # is NOT part of the release-please linked-versions group. The v2.0 milestone
  # advanced inbound to its own 2.0.0 (breaking schema-isolation contract), so the
  # floating constraint is `~> 2.0` — it must resolve against the inbound MAJOR
  # that ships alongside this admin release. A `==` pin (the shape release-please's
  # sed step writes for the linked siblings) could write an unsatisfiable
  # cross-line version, so this dep is deliberately ABSENT from the release-please
  # PINS array in `.github/workflows/release-please.yml`. Bump the floating line by
  # hand when inbound's major advances past core's (0.2 -> 1.x -> 2.0).
  #
  # The admin reads inbound rows exclusively through the
  # `MailglassAdmin.OptionalDeps.MailglassInbound` runtime gateway
  # (`Code.ensure_loaded?(MailglassInbound)` + `apply/3`), so `optional: true`
  # keeps the `--no-optional-deps` compile lane green with inbound stripped.
  defp mailglass_inbound_dep do
    if System.get_env("MIX_PUBLISH") == "true" do
      {:mailglass_inbound, "~> 2.0", optional: true}
    else
      {:mailglass_inbound, path: "../mailglass_inbound", optional: true}
    end
  end

  # REL-03: Semantic names are canonical. The `verify.phase_05` key below is a
  # deprecated one-cycle pass-through that delegates to the semantic alias.
  # Remove it in the next release cycle.
  #
  # Phase 5 verification gate. Intentionally RED at Plan 02 completion:
  #   - step 2 (test --warnings-as-errors) fails because Plans 03-06 tests are RED
  #   - step 3 (mailglass_admin.assets.build) fails because Plan 05 ships that task
  # Step 4 is the PREV-06 / CONTEXT D-04 merge gate — bundle drift CI check.
  defp aliases do
    [
      # Sibling-local ci verb (uniform "is this green?" across packages). The
      # root `mix ci` already fans out; these serve the inner-loop-in-a-subdir
      # case for a contributor working inside mailglass_admin/.
      "ci.fast": [
        "format --check-formatted",
        "compile --no-optional-deps --warnings-as-errors",
        "credo --strict"
      ],
      ci: [
        "ci.fast",
        "verify.support_contract.admin"
      ],
      # Semantic alias (REL-03)
      "verify.preview": [
        "compile --no-optional-deps --warnings-as-errors",
        "test --warnings-as-errors --exclude flaky",
        "mailglass_admin.assets.build",
        "cmd git diff --exit-code priv/static/"
      ],
      "verify.support_contract.admin": [
        "test --warnings-as-errors"
      ],
      # Deprecated pass-through (REL-03, one cycle) — use verify.preview instead
      "verify.phase_05": ["verify.preview"]
    ]
  end

  # CONTEXT D-04: strict files whitelist. `assets/` source is EXCLUDED from
  # the Hex tarball (vendored daisyUI + Tailwind input); `priv/static/` is
  # INCLUDED (compiled CSS, fonts, logo). LiveDashboard / Oban Web precedent.
  defp package do
    [
      name: "mailglass_admin",
      licenses: ["MIT"],
      description: @description,
      source_ref_pattern: "mailglass-sibling-group-v%{version}",
      links: %{
        "GitHub" => @source_url,
        "HexDocs" => "https://hexdocs.pm/mailglass_admin"
      },
      files: ~w(lib priv/static docs .formatter.exs mix.exs README* CHANGELOG* LICENSE*)
    ]
  end

  defp docs do
    [
      main: "MailglassAdmin",
      source_url: @source_url,
      source_ref: "v" <> @version,
      logo: "../brandbook/assets/logo-mark.svg",
      favicon: "../brandbook/assets/favicon.svg",
      extras: [
        "README.md",
        "docs/design-system.md",
        "docs/operator-trust.md",
        "docs/api_stability.md",
        "docs/compatibility-and-deprecations.md"
      ],
      groups_for_extras: [
        Overview: ["README.md"],
        Design: ["docs/design-system.md"],
        Contract: [
          "docs/operator-trust.md",
          "docs/api_stability.md",
          "docs/compatibility-and-deprecations.md"
        ]
      ],
      groups_for_modules: [
        Stable: [MailglassAdmin, MailglassAdmin.Router, MailglassAdmin.Auth],
        Internal: [MailglassAdmin.Operator.Mount]
      ]
    ]
  end
end