Some deps download artifacts at compile time: rustler_precompiled fetches the mdex_native NIF from GitHub during its first compile, and NimblePublisher pulls mdex_native into both apps. Compiling inside the deps target keeps that traffic inside setup - the explicit user-run network step - instead of the first make check/test on a fresh clone.
217 lines
10 KiB
Makefile
217 lines
10 KiB
Makefile
# Makefile for Firehose app
|
|
|
|
MISE_BIN ?= mise
|
|
MISE_EXEC = $(MISE_BIN) exec --
|
|
|
|
.PHONY: check precommit deps deps.check assets compile test test.coverage sobelow coverage crap format format.check credo verify
|
|
|
|
# Run all static analysis checks (no database, no network required). The
|
|
# deps.check guard fails loudly with a pointer at `make setup` when Hex deps
|
|
# are missing. `format.check` only reports: a gate that rewrites the files it
|
|
# judges can never fail. Fix formatting with `make format`.
|
|
check: deps.check credo format.check
|
|
|
|
# Precommit target for CI/pre-commit hooks
|
|
precommit: check compile
|
|
|
|
# Install JS dependencies (esbuild bundle, playwright). `mix phx.server` and
|
|
# `mix assets.deploy` need them: without `assets/node_modules` the bundle fails
|
|
# to resolve highlight.js and LiveView never loads.
|
|
#
|
|
# Guarded on npm's own install marker, so a clone pays for it once and later
|
|
# runs never touch the network: `make test` keeps working offline.
|
|
assets:
|
|
@test -f assets/node_modules/.package-lock.json || \
|
|
(echo "==> assets: npm install (first run)"; cd assets && npm install)
|
|
|
|
# Sync dependencies (Hex packages + JS packages) and compile them. The compile
|
|
# step is part of setup on purpose: some deps download artifacts at compile
|
|
# time - rustler_precompiled fetches the mdex_native NIF from GitHub during
|
|
# its first compile, and NimblePublisher pulls mdex_native into this app. Keep
|
|
# that traffic in this explicit, user-run step so check/test/verify stay
|
|
# offline even on a cold clone. (rustler_precompiled caches artifacts globally
|
|
# in ~/Library/Caches/rustler_precompiled, so later test-env compiles find
|
|
# them without network.)
|
|
deps: assets
|
|
$(MISE_EXEC) mix deps.get
|
|
$(MISE_EXEC) mix deps.compile
|
|
|
|
# Local, network-free guard: every package pinned in mix.lock must be present
|
|
# in deps/. check, test and test.coverage depend on this instead of `deps`, so
|
|
# the everyday loops never touch the network: `mix deps.get` contacts hex.pm
|
|
# even when everything is already installed (the advisory check), which fails
|
|
# in offline/sandboxed environments. Fetching is `make setup`'s explicit job,
|
|
# run from the repo root.
|
|
deps.check:
|
|
@test -f mix.lock || { echo "deps.check: no mix.lock - run 'make setup' at the repo root" >&2; exit 1; }
|
|
@missing=""; \
|
|
for dep in $$(sed -n 's/^[[:space:]]*"\([^"]*\)".*/\1/p' mix.lock); do \
|
|
[ -d "deps/$$dep" ] || missing="$$missing $$dep"; \
|
|
done; \
|
|
test -z "$$missing" || { echo "deps.check: missing:$$missing - run 'make setup' at the repo root" >&2; exit 1; }
|
|
|
|
# Compile the project
|
|
compile:
|
|
$(MISE_EXEC) mix compile --warnings-as-errors
|
|
|
|
# Run tests (requires PostgreSQL running on localhost:5432)
|
|
# Note: If you don't have PostgreSQL, you can skip tests with `make check`
|
|
test: deps.check compile
|
|
$(MISE_EXEC) mix test
|
|
|
|
# Run the suite once with coverage exported, so the coverage floor check (and,
|
|
# later, CRAP) reads the export instead of running the suite a second time.
|
|
# Stale exports are removed first: the floor must never report an older run.
|
|
test.coverage: deps.check compile
|
|
rm -f cover/*.coverdata
|
|
$(MISE_EXEC) mix test --cover --export-coverage default
|
|
|
|
# Enforce the coverage floor (mix.exs -> test_coverage) from that export. No
|
|
# test run. `mix test.coverage` exits 0 when it finds no export at all, so the
|
|
# guard below is load-bearing: a floor check that did not run must not pass.
|
|
coverage:
|
|
@test -f cover/default.coverdata || { echo "coverage: no exported coverage - run 'make test.coverage' first" >&2; exit 1; }
|
|
$(MISE_EXEC) mix test.coverage
|
|
|
|
# Enforce CRAP scores (complexity x coverage) from the same export. No test run.
|
|
# Runs in MIX_ENV=test because that is the environment the export came from:
|
|
# running in dev would compile a second build and score against a different set
|
|
# of modules. The guard is load-bearing for the same reason as `coverage`:
|
|
# ex_crap falls back to 0% coverage when the export is missing, which would
|
|
# report scores that are pessimistic instead of absent - a false failure is
|
|
# less bad than a false pass, but neither is acceptable in the Lens pass.
|
|
#
|
|
# Threshold is ex_crap's default (30), kept deliberately: decision 2026-10-28
|
|
# (human, yak "ratchets, deferred decisions, handoff practice") is not to
|
|
# ratchet --max-score down yet - below 30 is a policy change, not a cleanup.
|
|
# Baseline at that threshold, same date: 0 offenders above 30, worst function
|
|
# FirehoseWeb.CoreComponents.table/1 at 20.00 ("Summary: files=23 functions=140
|
|
# scored=140 worst_score=20.00"). A failing run prints "High scores: N" plus the
|
|
# offending functions; on a passing run read N=0 off the worst score rather than
|
|
# inventing a count.
|
|
crap:
|
|
@test -f cover/default.coverdata || { echo "crap: no exported coverage - run 'make test.coverage' first" >&2; exit 1; }
|
|
MIX_ENV=test $(MISE_EXEC) mix crap
|
|
|
|
# Format code. MUTATES the tree; deliberately not part of `check` (nor of the
|
|
# Lens pass), which must never write: a gate that fixes what it judges can never
|
|
# fail, and would dirty the tree a push is about to send. `make check` uses
|
|
# `format.check` below instead.
|
|
format:
|
|
$(MISE_EXEC) mix format
|
|
|
|
# Verify formatting without writing. Fails with the offending files, so the fix
|
|
# is explicit: run `make format` (or `make -C app format`).
|
|
format.check:
|
|
$(MISE_EXEC) mix format --check-formatted
|
|
|
|
# =============================================================================
|
|
# Playwright e2e browser tests
|
|
# =============================================================================
|
|
#
|
|
# Requires the Phoenix dev server running on the default port (8056).
|
|
# Set PLAYWRIGHT_BASE_URL to point at a different server/port.
|
|
#
|
|
# Available browsers (local):
|
|
# PLAYWRIGHT_BROWSER=firefox (default — works on macOS 15)
|
|
# PLAYWRIGHT_BROWSER=chromium (crashes on macOS 15, use Docker Chrome)
|
|
# PLAYWRIGHT_BROWSER=webkit
|
|
#
|
|
# Docker Chrome (alternative):
|
|
# 1. docker compose -f docker-compose.chrome.yml up -d
|
|
# 2. PLAYWRIGHT_BROWSERS_PATH=assets/node_modules/playwright-core/.local-browsers \
|
|
# npx playwright test --config=playwright.docker.config.ts
|
|
#
|
|
# =============================================================================
|
|
|
|
# Run Playwright e2e tests (default: local Firefox)
|
|
# Usage: make playwright
|
|
# PLAYWRIGHT_BROWSER=chromium make playwright
|
|
playwright:
|
|
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts
|
|
|
|
# Run a specific Playwright test file
|
|
# Usage: make playwright-test test/e2e/smoke.spec.ts
|
|
playwright-test:
|
|
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts $(filter-out $@,$(MAKECMDGOALS))
|
|
|
|
# Run Playwright tests with UI mode (headed browser)
|
|
playwright-ui:
|
|
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts --ui
|
|
|
|
# Show last Playwright HTML report
|
|
playwright-report:
|
|
cd assets && npx playwright show-report
|
|
|
|
# Start Docker Chrome container for remote Playwright
|
|
playwright-docker-start:
|
|
docker compose -f docker-compose.chrome.yml up -d
|
|
|
|
# Stop Docker Chrome container
|
|
playwright-docker-stop:
|
|
docker compose -f docker-compose.chrome.yml down
|
|
|
|
# Run Playwright against Docker Chrome (container must be running)
|
|
playwright-docker:
|
|
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.docker.config.ts
|
|
|
|
# Run full end-to-end workflow: start Docker Chrome, run tests, stop container
|
|
playwright-full: playwright-docker-start
|
|
@echo "Waiting for Chrome to be ready..."
|
|
@sleep 3
|
|
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.docker.config.ts; \
|
|
EXIT_CODE=$$?; \
|
|
cd ..; \
|
|
docker compose -f docker-compose.chrome.yml down; \
|
|
exit $$EXIT_CODE
|
|
|
|
# =============================================================================
|
|
# Static analysis
|
|
# =============================================================================
|
|
|
|
# Run Credo static analysis
|
|
credo:
|
|
$(MISE_EXEC) mix credo --strict
|
|
|
|
# =============================================================================
|
|
# Lens pass (heavy checks; root `make verify` runs this before every push)
|
|
# =============================================================================
|
|
#
|
|
# Sensor slots, cheapest first. Credo is cheap enough that it lives in
|
|
# `check`, not here. Add a sensor as its yak lands, both as a target and to
|
|
# VERIFY_SENSORS — that variable is the single source of truth for what this
|
|
# target reports as wired.
|
|
# - sobelow (yak: sobelow in the Lens pass)
|
|
# WIRED below (sobelow, ~1s). Fails loudly on findings and
|
|
# when the task is missing — no fallback of any kind.
|
|
# Per-finding skips exist only in .sobelow-skips, each with a
|
|
# written rationale (pinned by scripts/check-lens-sensors.sh).
|
|
# - coverage (yak: coverage floors + framework-glue exclusions policy)
|
|
# WIRED below (test.coverage + coverage).
|
|
# - crap (yak: CRAP scores with ex_crap (both apps))
|
|
# WIRED below (crap); reuses the coverage export.
|
|
# - mutation (yak: mutation testing with muex — a sensor, not a gate)
|
|
#
|
|
# Runtime note: the coverage sensor exports from a single suite run
|
|
# (`test.coverage` -> `mix test --cover --export-coverage default`) that CRAP
|
|
# reuses, instead of running the suite twice.
|
|
#
|
|
# The suite runs FIRST, because it is what produces the coverage export the
|
|
# sensors read; sensors still run cheapest-first among themselves.
|
|
VERIFY_SENSORS := sobelow coverage crap
|
|
|
|
# Sobelow security scan. `--exit` is load-bearing: without it sobelow prints
|
|
# findings and exits 0 (verified 2026-10-28, sobelow 0.17.0: 3 findings -> 0,
|
|
# `--exit` -> 1; it defaults to the `low` confidence threshold). `--skip`
|
|
# applies only the individually rationalised entries in .sobelow-skips.
|
|
# There is deliberately no guard or fallback: if the dep is absent, `mix
|
|
# sobelow` is a hard Mix error ("The task could not be found") and the
|
|
# verify loop's `|| exit 1` fails the pass.
|
|
sobelow:
|
|
$(MISE_EXEC) mix sobelow --skip --exit
|
|
|
|
verify:
|
|
@echo "==> verify(app): suite (coverage exported), then sensors [$(VERIFY_SENSORS)]"
|
|
@$(MAKE) test.coverage
|
|
@for sensor in $(VERIFY_SENSORS); do echo "==> verify(app): sensor $$sensor"; $(MAKE) $$sensor || exit 1; done
|