# 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