198 lines
9.1 KiB
Makefile
198 lines
9.1 KiB
Makefile
# DuckBrain development harness — Makefile
|
|
#
|
|
# This file (along with tests/, phpunit.xml, composer.*, .gitignore) lives
|
|
# ONLY on the develop branch. master is a publish-only vitrine that contains
|
|
# nothing but the readable artifact: nobody commits to master directly, and
|
|
# no file outside the WHITELIST below ever reaches it.
|
|
#
|
|
# Daily loop:
|
|
# make test run the unit suite (sqlite :memory:, no containers, no
|
|
# network; installs dev dependencies first if missing)
|
|
# ...then develop on develop and commit there as usual.
|
|
#
|
|
# Multi-engine regression net (run it whenever Model/Database change):
|
|
# make integration full matrix: brings the test services up,
|
|
# runs the Integration suite on sqlite, mysql
|
|
# and pgsql, then takes the services down even
|
|
# if a leg fails or you interrupt it
|
|
# make integration-engine ENGINE=mysql
|
|
# single leg (assumes services are already up);
|
|
# the fastest inner loop while iterating
|
|
# make db-up / make db-down manage the rootless podman test containers
|
|
# (mariadb:11, postgres:16-alpine); idempotent
|
|
#
|
|
# Hard port constraint: the services must bind default ports 3306/5432 on
|
|
# localhost because Database.php's DSN has no port field; stop any local
|
|
# server on those ports first (db-up aborts naming the port).
|
|
#
|
|
# Publishing ritual (human only — AI agents must never run it; it commits):
|
|
# 1. make publish MSG="feat: ..." (MSG optional, defaults to "sync: <date>")
|
|
# 2. review the new master commit (git log -1 refs/heads/master); if the
|
|
# generated message needs polish, rewrite it BEFORE pushing, e.g.
|
|
# from Emacs
|
|
# 3. git push origin master (manual on purpose: pushing is a
|
|
# decision, never a side effect)
|
|
#
|
|
# How publish builds master — wipe-and-rebuild mirror, no merges, no
|
|
# cherry-picks (see design decisions behind establish-dev-publish-workflow):
|
|
# - refuses to run off develop or with a dirty working tree
|
|
# - checks master out into a disposable worktree under .publish/
|
|
# (your current working tree is never touched)
|
|
# - for each WHITELIST path: removes master's copy and checks out develop's,
|
|
# so deleted/renamed artifact files never survive as ghosts in master
|
|
# - BLACKLIST then subtracts noise found INSIDE whitelisted paths
|
|
# - creates the commit only when the artifact actually changed; its body
|
|
# lists the develop commits (after the last-sync tag) that touched
|
|
# WHITELIST paths — pure-noise commits are filtered out automatically
|
|
# - moves the last-sync tag to develop's tip, removes the worktree, and
|
|
# prints any root entry left out of WHITELIST (add it if it belongs to
|
|
# the artifact)
|
|
|
|
SHELL := /bin/bash
|
|
|
|
.PHONY: test publish db-up db-up-mariadb db-up-postgres db-down integration integration-engine
|
|
|
|
PHPUNIT := vendor/bin/phpunit
|
|
|
|
# Engine for single-leg integration runs (services must already be up).
|
|
ENGINE ?= sqlite
|
|
|
|
# --- publish configuration -------------------------------------------------
|
|
WHITELIST := src config.php index.php autoload.php .htaccess readme.org
|
|
BLACKLIST :=
|
|
DEVELOP_BRANCH := develop
|
|
DEVELOP_REF := refs/heads/$(DEVELOP_BRANCH)
|
|
MASTER_BRANCH := master
|
|
MASTER_REF := refs/heads/$(MASTER_BRANCH)
|
|
LAST_SYNC_TAG := last-sync
|
|
MSG ?= sync: $(shell date +%Y-%m-%d)
|
|
|
|
# --- integration database services (podman rootless) -----------------------
|
|
# Default ports are non-negotiable: Database.php's DSN carries no port field,
|
|
# so the engines must answer on 3306/5432. If a local server owns those
|
|
# ports, stop it before `make db-up`.
|
|
MARIADB_NAME := duckbrain-test-mariadb
|
|
POSTGRES_NAME := duckbrain-test-postgres
|
|
MARIADB_IMAGE := docker.io/library/mariadb:11
|
|
POSTGRES_IMAGE := docker.io/library/postgres:16-alpine
|
|
TEST_DB_NAME := duckbrain_test
|
|
TEST_DB_USER := duckbrain
|
|
TEST_DB_PASS := duckbrain
|
|
DB_READY_SECS := 240
|
|
|
|
test:
|
|
@test -x $(PHPUNIT) || composer install --no-interaction
|
|
./$(PHPUNIT) --testsuite Unit
|
|
|
|
publish:
|
|
@if [ "$$(git branch --show-current)" != "$(DEVELOP_BRANCH)" ]; then \
|
|
echo "ABORT: publish must be invoked from $(DEVELOP_BRANCH) (current: '$$(git branch --show-current)')"; \
|
|
exit 1; \
|
|
fi
|
|
@if [ -n "$$(git status --porcelain)" ]; then \
|
|
echo "ABORT: working tree is dirty; commit your changes on $(DEVELOP_BRANCH) before publishing:"; \
|
|
git status --short; \
|
|
exit 1; \
|
|
fi
|
|
git worktree remove --force .publish/master 2>/dev/null || true
|
|
git worktree add --quiet .publish/master $(MASTER_BRANCH)
|
|
@test "$$(git -C .publish/master symbolic-ref HEAD)" = "$(MASTER_REF)" || { \
|
|
echo "ABORT: publish worktree HEAD is not attached to $(MASTER_REF)"; \
|
|
git worktree remove --force .publish/master; exit 1; }
|
|
@for w in $(WHITELIST); do git -C .publish/master rm -rf --ignore-unmatch --quiet -- $$w; done
|
|
git -C .publish/master checkout $(DEVELOP_REF) -- $(WHITELIST)
|
|
@for b in $(BLACKLIST); do git -C .publish/master rm -rf --ignore-unmatch --quiet -- $$b; done
|
|
git -C .publish/master diff --quiet $(DEVELOP_REF) -- $(WHITELIST)
|
|
@if git -C .publish/master diff --cached --quiet; then \
|
|
echo "nothing to publish: master already mirrors $(DEVELOP_BRANCH)"; \
|
|
else \
|
|
set -e; \
|
|
if git rev-parse -q --verify tags/$(LAST_SYNC_TAG) >/dev/null; then \
|
|
RANGE=$(LAST_SYNC_TAG)..$(DEVELOP_REF); \
|
|
else \
|
|
echo "note: no $(LAST_SYNC_TAG) tag yet; summarizing full $(DEVELOP_BRANCH) history"; \
|
|
RANGE=$(DEVELOP_REF); \
|
|
fi; \
|
|
BODY=$$(git log $$RANGE --oneline -- $(WHITELIST)); \
|
|
{ printf '%s\n' "$(MSG)"; printf '\n'; printf '%s\n' "$$BODY"; } > .publish/msg.txt; \
|
|
git -C .publish/master commit --quiet -F "$$(pwd)/.publish/msg.txt"; \
|
|
echo "publish commit created; review/edit it (before pushing) with your git tool of choice"; \
|
|
fi
|
|
git tag -f $(LAST_SYNC_TAG) $(DEVELOP_REF) >/dev/null
|
|
git worktree remove .publish/master
|
|
@echo "root entries left behind in $(DEVELOP_BRANCH) (add to WHITELIST if they belong to the artifact):"; \
|
|
comm -23 <(git ls-tree --name-only $(DEVELOP_REF) | sort) \
|
|
<(for x in $(WHITELIST) $(BLACKLIST); do echo $$x; done | sort) | sed 's/^/ - /'
|
|
|
|
db-up: db-up-mariadb db-up-postgres
|
|
|
|
db-up-mariadb:
|
|
@if podman ps -a --format '{{.Names}}' | grep -qx $(MARIADB_NAME); then \
|
|
echo "reusing $(MARIADB_NAME)"; podman start $(MARIADB_NAME) >/dev/null; \
|
|
else \
|
|
(echo > /dev/tcp/127.0.0.1/3306) 2>/dev/null && { \
|
|
echo "ABORT: port 3306 already in use; stop the local MySQL/MariaDB server or free the port before db-up"; exit 1; } || true; \
|
|
podman run -d --name $(MARIADB_NAME) \
|
|
-e MARIADB_ROOT_PASSWORD=$(TEST_DB_PASS) \
|
|
-e MARIADB_DATABASE=$(TEST_DB_NAME) \
|
|
-e MARIADB_USER=$(TEST_DB_USER) \
|
|
-e MARIADB_PASSWORD=$(TEST_DB_PASS) \
|
|
-p 127.0.0.1:3306:3306 $(MARIADB_IMAGE) >/dev/null; \
|
|
fi
|
|
@echo "waiting for $(MARIADB_NAME) to accept connections..."; \
|
|
for i in $$(seq 1 $(DB_READY_SECS)); do \
|
|
podman exec $(MARIADB_NAME) mariadb-admin ping -h 127.0.0.1 -u $(TEST_DB_USER) -p$(TEST_DB_PASS) --silent >/dev/null 2>&1 \
|
|
&& echo "$(MARIADB_NAME) ready" && exit 0; \
|
|
sleep 2; \
|
|
done; \
|
|
echo "TIMEOUT: $(MARIADB_NAME) never became ready; inspect: podman logs $(MARIADB_NAME)"; exit 1
|
|
|
|
db-up-postgres:
|
|
@if podman ps -a --format '{{.Names}}' | grep -qx $(POSTGRES_NAME); then \
|
|
echo "reusing $(POSTGRES_NAME)"; podman start $(POSTGRES_NAME) >/dev/null; \
|
|
else \
|
|
(echo > /dev/tcp/127.0.0.1/5432) 2>/dev/null && { \
|
|
echo "ABORT: port 5432 already in use; stop the local PostgreSQL server or free the port before db-up"; exit 1; } || true; \
|
|
podman run -d --name $(POSTGRES_NAME) \
|
|
-e POSTGRES_DB=$(TEST_DB_NAME) \
|
|
-e POSTGRES_USER=$(TEST_DB_USER) \
|
|
-e POSTGRES_PASSWORD=$(TEST_DB_PASS) \
|
|
-p 127.0.0.1:5432:5432 $(POSTGRES_IMAGE) >/dev/null; \
|
|
fi
|
|
@echo "waiting for $(POSTGRES_NAME) to accept connections..."; \
|
|
for i in $$(seq 1 $(DB_READY_SECS)); do \
|
|
podman exec $(POSTGRES_NAME) pg_isready -U $(TEST_DB_USER) -d $(TEST_DB_NAME) >/dev/null 2>&1 \
|
|
&& echo "$(POSTGRES_NAME) ready" && exit 0; \
|
|
sleep 2; \
|
|
done; \
|
|
echo "TIMEOUT: $(POSTGRES_NAME) never became ready; inspect: podman logs $(POSTGRES_NAME)"; exit 1
|
|
|
|
db-down:
|
|
@for c in $(MARIADB_NAME) $(POSTGRES_NAME); do \
|
|
if podman ps -a --format '{{.Names}}' | grep -qx $$c; then \
|
|
podman stop $$c >/dev/null 2>&1; podman rm $$c >/dev/null 2>&1; echo "removed $$c"; \
|
|
else \
|
|
echo "$$c not present"; \
|
|
fi; \
|
|
done
|
|
|
|
integration:
|
|
@bash -c ' \
|
|
trap "$(MAKE) db-down" EXIT; \
|
|
trap "exit 130" INT; \
|
|
trap "exit 143" TERM; \
|
|
$(MAKE) --no-print-directory db-up || exit 1; \
|
|
rc=0; \
|
|
for e in sqlite mysql pgsql; do \
|
|
echo "============ leg: $$e ============"; \
|
|
$(MAKE) --no-print-directory integration-engine ENGINE=$$e || rc=1; \
|
|
done; \
|
|
echo "=================================="; \
|
|
if [ $$rc -eq 0 ]; then echo "integration: all legs green"; else echo "integration: at least one leg FAILED (rc=1)"; fi; \
|
|
exit $$rc \
|
|
'
|
|
|
|
integration-engine:
|
|
@test -x $(PHPUNIT) || composer install --no-interaction
|
|
DUCKBRAIN_TEST_DB=$(ENGINE) ./$(PHPUNIT) --testsuite Integration --cache-directory .phpunit.cache/$(ENGINE)
|