Files
firehose/app/Makefile
T
Firehose Bot 97fc024a38 add make setup and network-free deps.check guard
mix deps.get contacts hex.pm even when everything is already installed
(the advisory check), so the everyday loops failed in offline/sandboxed
environments; and the root deps target only installed app's deps, never
blogex's.

make setup (root) installs Hex deps for both apps plus the app's JS
packages, once per clone, explicitly. check, test and test.coverage in
both apps now depend on a local deps.check guard instead: every package
pinned in mix.lock must exist in deps/, else fail loudly pointing at
make setup. AGENTS.md documents the new target.
2026-10-29 17:44:16 +00:00

209 lines
9.7 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)
deps: assets
$(MISE_EXEC) mix deps.get
# 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