Files
devx/src/devx/make/devx.mak
T

427 lines
19 KiB
Makefile

# devx.mak — Shared Makefile fragment for devx-integrated projects.
#
# This fragment provides common targets for:
# - Vikunja task management and PR creation
# - Workflow validation (actionlint, act_runner)
# - Linting (ruff, pyright, bandit, pip-audit)
# - CI failure notification
# - Environment setup (venv, .env, hooks)
# - Test execution and quality checks
#
# Project config (task prefix, Vikunja project ID, repo owner, repo name)
# is read from [tool.devx] in pyproject.toml by devx.config — no
# Makefile variables needed.
#
# Usage in your Makefile:
#
# # Set DEVX_PYTHON to your venv's Python
# DEVX_PYTHON := $(BIN)/python
#
# # Include the devx fragment (silent if devx not installed yet)
# DEVX_MAK := $(shell $(DEVX_PYTHON) -c \
# "from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
# 2>/dev/null)
# -include $(DEVX_MAK)
#
# If devx is not installed, the -include silently skips and the targets
# are simply unavailable (run 'make setup' first).
#
# Variables (set BEFORE including this fragment):
# DEVX_PYTHON — Python executable (default: python3)
# DEVX_PR_BASE — PR base branch (default: master)
# DEVX_VENV — venv directory name (default: .venv)
# DEVX_BIN — venv bin directory (default: $(DEVX_VENV)/bin)
# DEVX_LINT_PATHS — paths for ruff/bandit (default: src/ tests/)
# DEVX_TYPECHECK_PATHS — paths for pyright (default: empty — uses pyright config)
# DEVX_COV_PKG — coverage package name (default: src/devx)
# DEVX_TEST_PATHS — pytest paths (default: tests/)
# DEVX_GITEA_PYPI_HOST — Gitea PyPI host (default: git.oblachno.oblachno.fyi)
# DEVX_GITEA_PYPI_ORG — Gitea PyPI org (default: oblachno-oss)
# DEVX_ACTIONLINT_CFG — actionlint config file (default: .gitea/actionlint.yaml)
# DEVX_WORKFLOW_DIR — workflow directory (default: .gitea/workflows)
DEVX_PYTHON ?= python3
DEVX_PR_BASE ?= master
DEVX_VENV ?= .venv
DEVX_BIN ?= $(DEVX_VENV)/bin
DEVX_LINT_PATHS ?= src/ tests/
DEVX_COV_PKG ?= src/devx
DEVX_TEST_PATHS ?= tests/
DEVX_GITEA_PYPI_HOST ?= git.oblachno.oblachno.fyi
DEVX_GITEA_PYPI_ORG ?= oblachno-oss
DEVX_ACTIONLINT_CFG ?= .gitea/actionlint.yaml
DEVX_WORKFLOW_DIR ?= .gitea/workflows
DEVX_DOCKERFILE_PATHS ?= docker
# PIP_INSTALL — helper to run pip with Gitea private PyPI registry configured.
# Usage: $(DEVX_PIP_INSTALL) install -e '.[ci,lint]'
# CI_GITEA_USERNAME can be set in .env, as an env var, or as a Make variable.
# Projects can alias: PIP_INSTALL = $(DEVX_PIP_INSTALL)
DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
_PYPI_USER="$${CI_GITEA_USERNAME:-emil}"; \
if [ -n "$$CI_GITEA_TOKEN" ] && [ -n "$$_PYPI_USER" ]; then export PIP_EXTRA_INDEX_URL="https://$$_PYPI_USER:$$CI_GITEA_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
$(DEVX_BIN)/pip
# ── Virtual environment management ────────────────────────────────────────────
#
# These targets provide a single, consistent venv setup across all
# devx-integrated projects (infra, grm, devx). Each project includes
# devx.mak and aliases its local targets to these.
#
# The venv is a standard .venv directory (no pyenv virtualenv dependency).
# pyenv can still be used to install Python 3.12+ but the venv itself
# is created with `python3 -m venv .venv`.
#
# Projects should set these variables BEFORE including devx.mak:
# DEVX_VENV — venv directory (default: .venv)
# DEVX_BIN — venv bin directory (default: $(DEVX_VENV)/bin)
# DEVX_PYTHON — Python executable (default: python3; should be $(DEVX_BIN)/python after setup)
#
# Common aliases in project Makefiles:
# PIP_INSTALL = $(DEVX_PIP_INSTALL)
# venv: devx-venv
# activate-scripts: devx-activate-scripts
# .env: devx-env
# Create .venv with Python version check (3.12+ required)
$(DEVX_VENV)/bin/activate:
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
python3 -m venv $(DEVX_VENV)
$(DEVX_BIN)/pip install --upgrade pip setuptools wheel
# Alias: devx-venv creates the venv (delegates to the activate rule)
devx-venv: $(DEVX_VENV)/bin/activate
# Ensure a venv exists — in CI (no pyenv), creates .venv if missing.
# Locally, uses the existing .venv (created by `make setup` or `make devx-venv`).
devx-ensure-venv:
@if [ ! -f $(DEVX_BIN)/python ]; then \
echo "[ensure-venv] Creating $(DEVX_VENV) (no venv found)..."; \
python3 -m venv $(DEVX_VENV); \
$(DEVX_BIN)/pip install --upgrade pip setuptools wheel; \
fi
.PHONY: devx-create-task devx-create-pr devx-push devx-push-with-pr devx-check-config
.PHONY: devx-pr-status devx-pr-logs devx-pr-label devx-pr-review devx-rebase devx-pr-rebase
.PHONY: devx-configure-gitea-pypi devx-install-tools devx-install-checkmake devx-checkmake
.PHONY: devx-workflow-lint devx-workflow-dryrun devx-workflow-dryrun-safe devx-workflow-check
.PHONY: devx-notify-failure devx-install-hooks devx-activate-scripts devx-venv devx-ensure-venv
.PHONY: devx-lint-ruff devx-lint-format devx-typecheck devx-lint-bandit devx-lint-deps devx-lint
.PHONY: devx-clean devx-pre-push
.PHONY: devx-check-mutable-globals devx-check-dep-docs devx-check-test-coverage devx-check-docs devx-check-test-speed
.PHONY: devx-check-api-identity-checks devx-setup-ssh-key
.PHONY: devx-test-unit devx-pytest-cov
.PHONY: devx-setup-image devx-lint-dockerfiles
# ── Vikunja task and PR management ────────────────────────────────────────────
# Create a Vikunja task (project ID read from [tool.devx] in pyproject.toml)
devx-create-task:
@$(DEVX_PYTHON) -m devx.tools.create_task
# Create a PR with title auto-derived from the Vikunja task
# (owner/repo read from [tool.devx] in pyproject.toml)
devx-create-pr:
@$(DEVX_PYTHON) -m devx.tools.create_pr --base $(DEVX_PR_BASE)
# Push current branch to origin
devx-push:
@git push -u origin HEAD
# Validate devx configuration in pyproject.toml
devx-check-config:
@$(DEVX_PYTHON) -m devx.tools.check_config
# Push and create PR in one step
devx-push-with-pr: devx-push devx-create-pr
# Check CI status for a PR (auto-detects current branch's PR)
# Usage: make devx-pr-status
# make devx-pr-status PR=42
# make devx-pr-status PR=42 WAIT=1 TIMEOUT=600
devx-pr-status:
@$(DEVX_PYTHON) -m devx.tools.pr_status \
$(if $(PR),--pr $(PR)) \
$(if $(WAIT),--wait) \
$(if $(TIMEOUT),--timeout $(TIMEOUT))
# Fetch logs for failed CI jobs on a PR
# Usage: make devx-pr-logs
# make devx-pr-logs PR=42
# make devx-pr-logs PR=42 JOB=quality TAIL=50
devx-pr-logs:
@$(DEVX_PYTHON) -m devx.tools.pr_logs \
$(if $(PR),--pr $(PR)) \
$(if $(JOB),--job $(JOB)) \
$(if $(TAIL),--tail $(TAIL))
# Add a label to a PR (default: ready-to-merge)
# Usage: make devx-pr-label
# make devx-pr-label PR=42
# make devx-pr-label PR=42 LABEL=ready-to-merge
devx-pr-label:
@$(DEVX_PYTHON) -m devx.tools.pr_label \
$(if $(PR),--pr $(PR)) \
--label $(or $(LABEL),ready-to-merge)
# Usage: make devx-pr-review PR=42 EVENT=APPROVE BODY="..." CHECKLIST=1,2,3,4,5,6,7,8,9,10,11,12,13
# make devx-pr-review PR=42 EVENT=REQUEST_CHANGES BODY="..."
# make devx-pr-review PR=42 (auto review)
devx-pr-review:
@$(DEVX_PYTHON) -m devx.ci.pr_review \
$(PR) $(DEVX_REPO_OWNER)/$(DEVX_REPO_NAME) \
$(if $(EVENT),--event $(EVENT)) \
$(if $(BODY),--body "$(BODY)") \
$(if $(CHECKLIST),--checklist-confirmed --checklist-categories $(CHECKLIST))
# Rebase current branch onto origin/master and force-push
# Usage: make devx-rebase
# make devx-rebase NO_PUSH=1
devx-rebase:
@$(DEVX_PYTHON) -m devx.tools.rebase \
$(if $(NO_PUSH),--no-push)
# Rebase a PR's head branch via Gitea API (server-side, no local git needed)
# Usage: make devx-pr-rebase
# make devx-pr-rebase PR=42
devx-pr-rebase:
@$(DEVX_PYTHON) -m devx.tools.pr_rebase \
$(if $(PR),--pr $(PR))
# ── Environment setup ─────────────────────────────────────────────────────────
# Configure Gitea private PyPI registry so pip can find devx and other
# private packages. In CI, CI_GITEA_TOKEN is set as a secret. Locally, it's in .env.
devx-configure-gitea-pypi:
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
if [ -z "$$CI_GITEA_TOKEN" ]; then echo "[configure-gitea-pypi] CI_GITEA_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
echo "[configure-gitea-pypi] Gitea PyPI registry configured (CI_GITEA_TOKEN present)."
# Create .env from .env.example if it doesn't exist
devx-env:
@if [ ! -f .env ]; then \
cp .env.example .env; \
echo "Created .env from .env.example — please edit it with your credentials."; \
fi
# Create activate scripts for shell/fish/zsh
devx-activate-scripts:
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
@test -f activate.fish || (echo '#!/usr/bin/env fish' > activate.fish && echo 'set -l script_dir (dirname (status --current-filename))' >> activate.fish && echo 'source "$$script_dir/.venv/bin/activate.fish"' >> activate.fish && chmod +x activate.fish)
@test -f activate.zsh || (echo '#!/usr/bin/env zsh' > activate.zsh && echo '0="$${ZERO:-$${0:#$$ZSH_ARGZERO}}"' >> activate.zsh && echo '0="$${$${(M)0:#/*}:-$$PWD/$$0}"' >> activate.zsh && echo 'source "$${0:A:h}/.venv/bin/activate"' >> activate.zsh && chmod +x activate.zsh)
# Set git hooks path to hooks/
devx-install-hooks:
@git config core.hooksPath hooks
@chmod +x hooks/pre-commit hooks/pre-push 2>/dev/null || true
@echo "core.hooksPath set to hooks/ — tracked hooks are now live."
# ── Tool installation ─────────────────────────────────────────────────────────
# Install CI/CD tools (actionlint, git-cliff, act_runner, tea) to ~/.local/bin
devx-install-tools:
@$(DEVX_PYTHON) -m devx.tools.install_tools
# Install checkmake (Makefile linter)
devx-install-checkmake:
@$(DEVX_PYTHON) -m devx.tools.install_checkmake
# Lint Makefiles with checkmake
devx-checkmake:
@CHECKMAKE_EXE="$$(command -v checkmake 2>/dev/null || echo $(HOME)/.local/bin/checkmake)"; \
if ! command -v "$$CHECKMAKE_EXE" >/dev/null 2>&1 && ! [ -x "$$CHECKMAKE_EXE" ]; then \
echo "[checkmake] checkmake not found. Run: make devx-install-checkmake"; exit 1; \
fi; \
"$$CHECKMAKE_EXE" $(CURDIR)/Makefile
# ── Workflow validation ───────────────────────────────────────────────────────
# Static lint of Gitea Actions workflow YAML files
devx-workflow-lint:
@command -v actionlint >/dev/null 2>&1 || { \
echo "actionlint not found. Install: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)"; \
exit 1; \
}
actionlint -config-file $(DEVX_ACTIONLINT_CFG) $(DEVX_WORKFLOW_DIR)/*.yml
# Dry-run all workflows (requires act_runner)
devx-workflow-dryrun:
@command -v act_runner >/dev/null 2>&1 || { echo "act_runner not found. Install: https://gitea.com/gitea/act_runner/releases"; exit 1; }
@echo "Dry-running all workflows (no Docker containers started)..."
act_runner exec --dryrun -W $(DEVX_WORKFLOW_DIR)/ 2>&1 | grep -E 'DRYRUN|ERROR|FAIL|Job'
# Best-effort dry-run (skips if act_runner is not installed)
devx-workflow-dryrun-safe:
@command -v act_runner >/dev/null 2>&1 && { echo "Dry-running workflows..."; act_runner exec --dryrun -W $(DEVX_WORKFLOW_DIR)/ 2>&1 | grep -E 'DRYRUN|ERROR|FAIL|Job'; } || echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
# Static lint + dry-run
devx-workflow-check: devx-workflow-lint devx-workflow-dryrun
@echo "Workflow checks passed (static lint + dry-run)."
# ── CI failure notification ───────────────────────────────────────────────────
# Notify on CI failure — creates a Gitea issue via devx.ci.notify_failure.
# Usage: make devx-notify-failure WORKFLOW=post-merge/release
# Requires: CI_GITEA_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA
devx-notify-failure:
@. $(DEVX_VENV)/bin/activate 2>/dev/null || true; \
export PATH="$(HOME)/.local/bin:$$PATH"; \
$(DEVX_PYTHON) -m devx.tools.install_tools --tool tea 2>/dev/null || true; \
$(DEVX_PYTHON) -m devx.ci.notify_failure --auto-login \
--repo "$${GITHUB_REPOSITORY}" \
--run-id "$${GITHUB_RUN_ID}" \
--workflow "$(WORKFLOW)" \
--commit "$${GITHUB_SHA}"
# ── Linting ───────────────────────────────────────────────────────────────────
devx-lint-ruff:
@$(DEVX_BIN)/ruff check $(DEVX_LINT_PATHS)
devx-lint-format:
@$(DEVX_BIN)/ruff format --check $(DEVX_LINT_PATHS)
devx-typecheck:
@$(DEVX_BIN)/pyright
devx-lint-bandit:
@$(DEVX_BIN)/bandit -r src/
devx-lint-deps:
@echo "Checking dependencies for known vulnerabilities..."
@$(DEVX_BIN)/python -m ensurepip 2>/dev/null || true
@PIPAPI_PYTHON_LOCATION=$$(pwd)/$(DEVX_VENV)/bin/python \
$(DEVX_BIN)/pip-audit --desc --skip-editable 2>&1 || true
devx-lint: devx-lint-ruff devx-lint-format devx-typecheck devx-lint-bandit
@echo "[devx-lint] Linting checks passed."
# ── Testing ───────────────────────────────────────────────────────────────────
devx-test-unit:
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -q --no-cov
devx-pytest-cov:
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=$(DEVX_COV_PKG) --cov-report=term-missing --cov-fail-under=100
# ── Quality checks ────────────────────────────────────────────────────────────
# Scan for module-level mutable globals that cause test isolation bugs
devx-check-mutable-globals:
@$(DEVX_PYTHON) -m devx.tools.check_mutable_globals
# Validate that every dependency in pyproject.toml has a documented purpose
devx-check-dep-docs:
@$(DEVX_PYTHON) -m devx.tools.check_pyproject_deps
# Check that changed files have corresponding tests
devx-check-test-coverage:
@$(DEVX_PYTHON) -m devx.tools.check_test_coverage
# Validate agent and user docs for stale file references
devx-check-docs:
@$(DEVX_PYTHON) -m devx.tools.check_agent_docs
# Verify test suite timing
devx-check-test-speed:
@$(DEVX_PYTHON) -m devx.tools.check_test_speed
# Scan integration tests for unsafe is True/is False identity checks
devx-check-api-identity-checks:
@$(DEVX_PYTHON) -m devx.tools.check_api_identity_checks
# Set up SSH private key from SSH_PRIVATE_KEY env var
devx-setup-ssh-key:
@$(DEVX_PYTHON) -m devx.tools.setup_ssh_key
# ── Pre-push validation ───────────────────────────────────────────────────────
# Run lint + tests before push (projects can override with project-specific targets)
devx-pre-push: devx-lint devx-pytest-cov
@echo "[devx-pre-push] All checks passed. Proceeding with push."
# ── Cleanup ───────────────────────────────────────────────────────────────────
devx-clean:
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
@rm -rf .coverage htmlcov/ dist/ build/ *.egg-info/ .molecule/ 2>/dev/null || true
# ── Dockerfile linting ────────────────────────────────────────────────────────
#
# Lint Dockerfiles with hadolint. Fails fast if hadolint is not installed
# (no silent skip). Set DEVX_DOCKERFILE_PATHS to the directory containing
# your Dockerfiles (default: docker).
#
# Usage:
# make devx-lint-dockerfiles (lints docker/ directory)
# make devx-lint-dockerfiles DEVX_DOCKERFILE_PATHS=ansible (lints ansible/)
devx-lint-dockerfiles:
@echo "[devx-lint-dockerfiles] Linting Dockerfiles with hadolint..."
@if ! command -v hadolint >/dev/null 2>&1; then \
echo "[devx-lint-dockerfiles] ERROR: hadolint not found. Install from https://github.com/hadolint/hadolint/releases" >&2; \
exit 1; \
fi
@find $(DEVX_DOCKERFILE_PATHS) -name 'Dockerfile*' -exec hadolint {} +
@echo "[devx-lint-dockerfiles] All Dockerfiles passed."
# ── Pre-built image setup ─────────────────────────────────────────────────────
#
# When running inside a pre-built Docker runner image (ci-base, ci-quality,
# ci-full), all deps are already installed in /opt/venv. This target links
# the venv and installs the project itself (with optional extras).
#
# Usage:
# make devx-setup-image (runtime deps only)
# make devx-setup-image EXTRAS=lint (runtime + lint deps)
# make devx-setup-image EXTRAS=ci,lint (runtime + ci + lint deps)
#
# Falls back to setup-ci if /opt/venv is not present (local dev).
# Note: the fallback target name is project-specific (setup-ci, not
# devx-setup-ci) — each project defines its own setup-ci target.
devx-setup-image:
@/opt/venv/bin/python -m devx.tools.setup_image --venv $(DEVX_VENV) --extras "$(EXTRAS)" \
--gitea-host $(DEVX_GITEA_PYPI_HOST) --gitea-org $(DEVX_GITEA_PYPI_ORG)
# ── Docker image build / push / cleanup ───────────────────────────────────────
#
# Variables:
# DEVX_GITEA_REGISTRY — registry URL (default: git.oblachno.oblachno.fyi)
# DEVX_IMAGE_MANIFEST — path to JSON manifest (default: docker/images.json)
# DEVX_IMAGE_OWNER — package owner for cleanup (default: oblachno-oss)
DEVX_GITEA_REGISTRY ?= git.oblachno.oblachno.fyi
DEVX_IMAGE_MANIFEST ?= docker/images.json
DEVX_IMAGE_OWNER ?= oblachno-oss
# Build all images from manifest (no push)
devx-build-images:
@$(DEVX_PYTHON) -m devx.tools.build_image --manifest $(DEVX_IMAGE_MANIFEST) --pull
# Build and push all images to the Gitea registry
devx-push-images:
@$(DEVX_PYTHON) -m devx.tools.build_image \
--manifest $(DEVX_IMAGE_MANIFEST) \
--registry $(DEVX_GITEA_REGISTRY) \
--push --pull
# Dry-run: show what would be built/pushed
devx-build-images-dry-run:
@$(DEVX_PYTHON) -m devx.tools.build_image \
--manifest $(DEVX_IMAGE_MANIFEST) \
--registry $(DEVX_GITEA_REGISTRY) \
--push --dry-run
# Clean up old image versions (keep last 2 + latest)
devx-clean-images:
@$(DEVX_PYTHON) -m devx.tools.clean_images \
--owner $(DEVX_IMAGE_OWNER) \
--name oblachno-oss/runner-images/ci-base \
--name oblachno-oss/runner-images/ci-quality \
--name oblachno-oss/runner-images/ci-full \
--keep 2