# Makefile for Octop # Usage: # make - Show this help # make all - format (BE+FE) + backend lint + typecheck + test (ship bar) # make build - Build frontend + Python wheel # make publish - Build + upload to PyPI # # Prerequisites: # - Node.js + npm (frontend) # - Python 3.12+ (backend) # - uv (recommended) or pip # - twine (publish: pip install twine / make install-tools) SHELL := /bin/bash .DEFAULT_GOAL := help REPO_ROOT := $(shell pwd) DASHBOARD_DIR := $(REPO_ROOT)/dashboard DASHBOARD_DEST := $(REPO_ROOT)/src/octop/dashboard DIST_DIR := $(REPO_ROOT)/dist # PyPI repository (override: make publish PYPI_REPO=testpypi) PYPI_REPO ?= pypi PYPI_TOKEN ?= # Prefer uv when available; fall back to plain commands (CI / pip install -e ".[dev]") UV := $(shell command -v uv 2>/dev/null) RUN := $(if $(UV),uv run,) PYTHON := $(if $(UV),uv run python,python3) PIP := $(if $(UV),uv pip,python3 -m pip) # Parallel workers for make test / make test-fast (override: make test PYTEST_JOBS=4) PYTEST_JOBS ?= auto # ─── Help ──────────────────────────────────────────────────────────────────── .PHONY: help help: @echo "Octop Build System" @echo "" @echo "Usage: make " @echo "" @echo "Build targets:" @echo " build Build frontend + Python wheel (full package)" @echo " build-frontend Build React dashboard only → $(DASHBOARD_DEST)" @echo " build-wheel Build Python wheel only (assumes frontend built)" @echo "" @echo "Publish targets:" @echo " publish Build + upload to PyPI (PYPI_TOKEN or prompt)" @echo " publish-test Build + upload to TestPyPI" @echo "" @echo "Development targets:" @echo " dev Start frontend + backend dev servers" @echo " dev-frontend Start Vite dev server only" @echo " dev-backend Start octop run only" @echo "" @echo "Online-deps targets (local Octop source + PyPI harness components):" @echo " install-online Create .venv-online: Octop editable + harness-* from PyPI" @echo " test-online pytest against .venv-online (not live)" @echo " run-online Start octop run from .venv-online" @echo "" @echo "Quality targets (ship bar):" @echo " all format-all + lint + typecheck + test (backend lint/typecheck/test)" @echo " precommit fast change-aware gate for the git pre-commit hook (testmon-scoped tests)" @echo " lint Ruff check + format check (src, tests)" @echo " format Ruff auto-fix + format (src, tests)" @echo " typecheck mypy --strict src/octop" @echo " test pytest -m \"not live\"" @echo " test-live pytest -m live" @echo "" @echo "Quality targets (frontend):" @echo " lint-frontend ESLint + Prettier check" @echo " format-frontend Prettier write" @echo " typecheck-frontend tsc -b (project references)" @echo "" @echo "Quality targets (full stack):" @echo " lint-all lint + lint-frontend" @echo " format-all format + format-frontend (also first step of make all)" @echo " typecheck-all typecheck + typecheck-frontend" @echo " check-all lint-all + typecheck-all + test" @echo "" @echo "Utility targets:" @echo " install-hooks Point git to .githooks (pre-commit: make all + dashboard build)" @echo " install Install Python dev dependencies (alias: install-dev)" @echo " install-dev uv sync / pip install -e \".[dev]\"" @echo " install-tools Install build + twine for publishing" @echo " clean Remove build artifacts and caches" @echo " clean-online Remove the .venv-online environment" @echo " version Show current project version" # ─── Build ─────────────────────────────────────────────────────────────────── .PHONY: build build: build-frontend build-wheel .PHONY: build-frontend build-frontend: @echo "[build-frontend] Installing npm dependencies..." cd $(DASHBOARD_DIR) && npm ci @echo "[build-frontend] Building dashboard (output: $(DASHBOARD_DEST))..." cd $(DASHBOARD_DIR) && NODE_ENV=production NODE_OPTIONS="--max-old-space-size=2048" npm run build @echo "[build-frontend] Done." .PHONY: build-wheel build-wheel: @echo "[build-wheel] Generating README.pypi.md (CHANGELOG + README)..." @{ cat $(REPO_ROOT)/CHANGELOG.md; echo; cat $(REPO_ROOT)/README.md; } > $(REPO_ROOT)/README.pypi.md @cp $(REPO_ROOT)/pyproject.toml $(REPO_ROOT)/pyproject.toml.bak @$(PYTHON) -c "from pathlib import Path; p=Path('$(REPO_ROOT)/pyproject.toml'); p.write_text(p.read_text().replace('readme = \"README.md\"', 'readme = \"README.pypi.md\"', 1))" @echo "[build-wheel] Cleaning previous build artifacts..." rm -rf $(DIST_DIR)/* rm -rf $(REPO_ROOT)/build @echo "[build-wheel] Building wheel + sdist..." @set -e; \ status=0; \ if [ -n "$(UV)" ]; then \ uv build --out-dir $(DIST_DIR) . || status=$$?; \ else \ $(PIP) install --quiet build; \ $(PYTHON) -m build --no-isolation --outdir $(DIST_DIR) . || status=$$?; \ fi; \ mv $(REPO_ROOT)/pyproject.toml.bak $(REPO_ROOT)/pyproject.toml; \ rm -f $(REPO_ROOT)/README.pypi.md; \ exit $$status @echo "[build-wheel] Done. Artifacts in: $(DIST_DIR)/" @ls -lh $(DIST_DIR)/ # ─── Publish ───────────────────────────────────────────────────────────────── .PHONY: publish publish: build @echo "[publish] Uploading to $(PYPI_REPO)..." @if ! command -v twine > /dev/null 2>&1; then \ echo "[publish] twine not found. Installing..."; \ $(PIP) install --quiet twine; \ fi @_token="$(PYPI_TOKEN)"; \ if [ -z "$$_token" ]; then \ echo ""; \ printf " PYPI_TOKEN is not set. Enter your PyPI API token (pypi-xxx): "; \ read -r _token; \ if [ -z "$$_token" ]; then \ echo "[publish] No token provided. Aborting."; \ exit 1; \ fi; \ fi; \ $(if $(UV),uv run,) twine upload \ $(if $(filter testpypi,$(PYPI_REPO)),--repository testpypi,) \ --username __token__ --password "$$_token" \ $(DIST_DIR)/* @echo "[publish] Upload complete." .PHONY: publish-test publish-test: $(MAKE) publish PYPI_REPO=testpypi # ─── Development ───────────────────────────────────────────────────────────── .PHONY: dev dev: @echo "[dev] Starting frontend and backend dev servers (Ctrl-C to stop both)..." @trap 'kill 0' SIGINT; \ (cd $(DASHBOARD_DIR) && npm run dev) & \ ($(RUN) octop run) & \ wait .PHONY: dev-frontend dev-frontend: cd $(DASHBOARD_DIR) && npm run dev .PHONY: dev-backend dev-backend: $(RUN) octop run # ─── Online-deps dev (local Octop source + PyPI harness components) ─────────── # Keeps a dedicated .venv-online alongside .venv. Octop itself is installed # editable (-e) so source edits are live; the harness-* siblings are pulled # from PyPI (--no-sources ignores the editable local paths in uv.lock). # Re-run install-online only when a dependency version changes. VENV_ONLINE := $(REPO_ROOT)/.venv-online PY_ONLINE := $(VENV_ONLINE)/bin .PHONY: install-online install-online: @echo "[install-online] Creating $(VENV_ONLINE) (local Octop + PyPI harness deps)..." uv venv $(VENV_ONLINE) --python 3.12 uv pip install --no-sources --python $(PY_ONLINE)/python -e ".[dev]" @echo "[install-online] Done. Octop=editable(local), harness-*=PyPI." .PHONY: test-online test-online: @echo "[test-online] pytest against $(VENV_ONLINE) (not live)..." $(PY_ONLINE)/pytest -m "not live" .PHONY: run-online run-online: $(PY_ONLINE)/octop run # ─── Quality (backend) ─────────────────────────────────────────────────────── .PHONY: all all: format-all lint typecheck test .PHONY: lint lint: @echo "[lint] Ruff check..." $(RUN) ruff check src tests @echo "[lint] Ruff format check..." $(RUN) ruff format --check src tests .PHONY: format format: @echo "[format] Ruff fix + format..." $(RUN) ruff check --fix src tests $(RUN) ruff format src tests .PHONY: typecheck typecheck: @echo "[typecheck] mypy..." $(RUN) mypy src/octop .PHONY: test test: @echo "[test] pytest (not live, -n $(PYTEST_JOBS))..." $(RUN) pytest -n $(PYTEST_JOBS) -m "not live" .PHONY: test-fast test-fast: @echo "[test-fast] pytest (not live, not slow, -n $(PYTEST_JOBS))..." $(RUN) pytest -n $(PYTEST_JOBS) -m "not live and not slow" .PHONY: test-live test-live: @echo "[test] pytest (live)..." $(RUN) pytest -m live # Fast, change-aware gate for the local pre-commit hook. Runs ruff/mypy (cheap # and kept full for accuracy) plus only the tests affected by changed files via # pytest-testmon. The full suite still runs in CI (`make all`); this is local # feedback only, so a missed cross-module impact is caught there. .PHONY: precommit precommit: format-all lint typecheck test-affected # NOTE: do NOT pass `-m` here — pytest-testmon deactivates its affected-test # selection whenever a marker expression is present, which would fall back to # the full suite. Live tests under tests/live/ auto-skip when credentials are # absent, so running them unscoped is cheap and safe; the CI `make all` gate # keeps `-m "not live"` for the authoritative full run. # # NOTE: testmon detects changes via `git ls-files -m` (worktree vs index), which # is empty once changes are staged — exactly the pre-commit state — so it would # silently run zero tests. The patch in conftest.py / tests/support/ # testmon_staged_changes.py redirects detection to `git diff HEAD` (staged + # unstaged) so the gate actually fires on a commit's changes. .PHONY: test-affected test-affected: @echo "[test] pytest (testmon: only tests affected by changes)..." $(RUN) pytest --testmon # ─── Quality (frontend) ────────────────────────────────────────────────────── .PHONY: lint-frontend lint-frontend: @echo "[lint-frontend] ESLint..." cd $(DASHBOARD_DIR) && npm run lint @echo "[lint-frontend] Prettier check..." cd $(DASHBOARD_DIR) && npm run format:check .PHONY: format-frontend format-frontend: @echo "[format-frontend] Prettier..." cd $(DASHBOARD_DIR) && npm run format .PHONY: typecheck-frontend typecheck-frontend: @echo "[typecheck-frontend] tsc..." cd $(DASHBOARD_DIR) && npx tsc -b # ─── Quality (full stack) ──────────────────────────────────────────────────── .PHONY: lint-all lint-all: lint lint-frontend .PHONY: format-all format-all: format format-frontend .PHONY: typecheck-all typecheck-all: typecheck typecheck-frontend .PHONY: check-all check-all: lint-all typecheck-all test # ─── Utilities ─────────────────────────────────────────────────────────────── .PHONY: install-hooks install-hooks: @echo "[install-hooks] Setting core.hooksPath=.githooks" git config core.hooksPath .githooks @chmod +x "$(REPO_ROOT)/.githooks/"* 2>/dev/null || true @echo "[install-hooks] Done. Pre-commit will run: make precommit (format-all + lint + typecheck + testmon-scoped tests), dashboard build" @echo "[install-hooks] Bypass: SKIP_PRECOMMIT=1 git commit … or git commit --no-verify" .PHONY: install install-dev install install-dev: @echo "[install] Installing Python dev dependencies..." ifdef UV uv sync else $(PIP) install -e ".[dev]" endif .PHONY: install-tools install-tools: $(PIP) install --quiet build twine @echo "[install-tools] build and twine installed." .PHONY: clean clean: @echo "[clean] Removing build artifacts..." rm -rf $(DIST_DIR)/* rm -rf $(DASHBOARD_DIR)/dist rm -rf $(REPO_ROOT)/build rm -rf $(REPO_ROOT)/*.egg-info rm -rf $(REPO_ROOT)/src/*.egg-info rm -rf $(REPO_ROOT)/.mypy_cache $(REPO_ROOT)/.ruff_cache $(REPO_ROOT)/.pytest_cache rm -rf $(REPO_ROOT)/htmlcov find $(REPO_ROOT)/src -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true @echo "[clean] Done." .PHONY: clean-online clean-online: @echo "[clean-online] Removing $(VENV_ONLINE)..." rm -rf $(VENV_ONLINE) @echo "[clean-online] Done." .PHONY: version version: @$(PYTHON) -c "import pathlib, re; t = pathlib.Path('pyproject.toml').read_text(); m = re.search(r'^version\\s*=\\s*\"([^\"]+)\"', t, re.M); print(m.group(1) if m else 'unknown')"