chore(makefile): add worktree-new/worktree-rm targets for isolated dev servers

Encode the dual-repo worktree SOP as make targets so no step is skipped:
worktree-new creates the outer worktree off main, inits its own user/
submodule, branches both repos, and starts an isolated Grav dev server on an
auto-picked free port (8090+) whose identity is persisted in a git-ignored
.worktree-env; worktree-rm tears it all down including the submodule deinit
that, when skipped by hand, leaves orphaned .worktrees/ dirs.

docker-compose.yml container_name + ports are parametrized as ${VAR:-default}
so the main checkout is byte-identical, and the 11 hardcoded intotheeast_grav
refs in local targets now use $(GRAV_CONTAINER). CLAUDE.md points at the
commands instead of the manual steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
This commit is contained in:
2026-07-08 10:56:07 +02:00
co-authored by Claude Opus 4.8
parent 793ca10d4d
commit c4891d8f60
4 changed files with 78 additions and 15 deletions
+70 -11
View File
@@ -3,6 +3,12 @@
# for ${UID}/${GID} substitution and the travel-memories env_file.)
-include .env
# Per-worktree dev-server identity, written by `make worktree-new` into the new
# worktree only (git-ignored). Absent in the main checkout, so the defaults below
# apply there. Loaded here so every local target + compose call in a worktree
# targets that worktree's own container and ports.
-include .worktree-env
# Remote config — loaded only when targeting an environment. ENV is set
# automatically by the env-suffixed remote targets (e.g. `make remote-install-prod`);
# each .env.<ENV> holds a full, self-contained set of remote vars.
@@ -49,7 +55,7 @@ GRAV_TEST_USER ?= testrunner
GRAV_TEST_PASS ?= Testpass1234
test-account:
@docker exec intotheeast_grav sh -c 'test -f /var/www/html/user/accounts/$(GRAV_TEST_USER).yaml \
@docker exec $(GRAV_CONTAINER) sh -c 'test -f /var/www/html/user/accounts/$(GRAV_TEST_USER).yaml \
|| php bin/plugin login new-user -u $(GRAV_TEST_USER) -p "$(GRAV_TEST_PASS)" \
-e $(GRAV_TEST_USER)@example.test -N "Test Runner" -P b --admin-type both -s enabled -n'
@@ -66,6 +72,13 @@ test: test-config test-post test-ui
# ── Local dev ──────────────────────────────────────────────────────────────────
# Dev-server identity. Defaults are the main checkout's canonical values; a
# worktree's .worktree-env (above) overrides them so servers never collide.
# Exported (via the top-of-file `export`) so `docker compose` picks them up.
GRAV_CONTAINER ?= intotheeast_grav
GRAV_PORT ?= 8081
TM_PORT ?= 8082
build:
docker compose build
@@ -78,19 +91,24 @@ build-assets:
start:
docker compose up -d
# Grav service only — used by `make worktree-new` (a worktree rarely needs the
# travel-memories service, and this keeps its footprint minimal).
start-grav:
docker compose up -d grav
stop:
docker compose down
setup: build start install-plugins fix-perms
fix-perms:
docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser"
docker exec intotheeast_grav chown -R 1000:1000 /var/www/html
docker exec intotheeast_grav apachectl graceful
docker exec $(GRAV_CONTAINER) bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser"
docker exec $(GRAV_CONTAINER) chown -R 1000:1000 /var/www/html
docker exec $(GRAV_CONTAINER) apachectl graceful
install-plugins:
docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
docker exec -w /var/www/html $(GRAV_CONTAINER) php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
$(MAKE) apply-plugin-patches
# Re-apply local fixes to git-ignored, GPM-managed third-party plugins. Run this
@@ -105,13 +123,54 @@ apply-plugin-patches:
fi; \
done
# ── Worktrees ─────────────────────────────────────────────────────────────────
# Isolated outer-repo worktree + its own user/ submodule checkout + its own dev
# server (distinct container name & ports), for long-running feature work that
# runs in parallel with the main checkout without collisions. Encodes the full
# SOP from docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md
# so no step (submodule init, per-server isolation, clean teardown) is skipped.
#
# make worktree-new NAME=my-feature [PORT=8090] # create branch + start server
# make worktree-rm NAME=my-feature # tear down cleanly
#
# Run both from the MAIN checkout. After worktree-new, `cd .worktrees/<name>`
# and use make as normal — it targets that worktree's own server automatically.
WT_DIR = .worktrees/$(NAME)
guard-name:
@test -n "$(NAME)" || { echo "ERROR: set NAME=, e.g. 'make worktree-new NAME=my-feature'."; exit 1; }
worktree-new: guard-name
@test ! -e "$(WT_DIR)" || { echo "ERROR: $(WT_DIR) already exists."; exit 1; }
git worktree add "$(WT_DIR)" -b feat/$(NAME) main
git -C "$(WT_DIR)" submodule update --init user
git -C "$(WT_DIR)/user" checkout -b feat/$(NAME)
@port=$${PORT:-$$(for p in $$(seq 8090 8099); do \
docker ps --format '{{.Ports}}' | grep -q ":$$p->" || { echo $$p; break; }; \
done)}; \
test -n "$$port" || { echo "ERROR: no free port in 8090-8099; pass PORT= explicitly."; exit 1; }; \
printf 'COMPOSE_PROJECT_NAME=itte-%s\nGRAV_CONTAINER=itte_%s_grav\nGRAV_PORT=%s\nTM_PORT=%s\n' \
"$(NAME)" "$(NAME)" "$$port" "$$((port + 100))" > "$(WT_DIR)/.worktree-env"; \
echo "→ starting this worktree's Grav dev server on http://localhost:$$port"; \
$(MAKE) -C "$(WT_DIR)" start-grav
@echo "Worktree ready: $(WT_DIR) (outer + user/ on branch feat/$(NAME))"
worktree-rm: guard-name
@test -e "$(WT_DIR)" || { echo "ERROR: $(WT_DIR) does not exist."; exit 1; }
-$(MAKE) -C "$(WT_DIR)" stop
-git -C "$(WT_DIR)" submodule deinit -f user
git worktree remove --force "$(WT_DIR)"
git worktree prune
@echo "Removed $(WT_DIR). If feat/$(NAME) is merged, drop it: git branch -d feat/$(NAME)"
# ── Demo content ──────────────────────────────────────────────────────────────
demo-load:
# Load every fixture trip under docs/demo/trips/ into the pages tree.
# Source uses dailies/ + 04.stories/; dailies/ maps to 01.dailies/ on copy.
# All copies are `|| true` so a fixture absent from an older user/ is skipped.
docker exec intotheeast_grav bash -c 'for src in /var/www/html/user/docs/demo/trips/*/; do \
docker exec $(GRAV_CONTAINER) bash -c 'for src in /var/www/html/user/docs/demo/trips/*/; do \
slug=$$(basename "$$src"); dst=/var/www/html/user/pages/01.trips/$$slug; \
mkdir -p "$$dst/01.dailies" "$$dst/04.stories"; \
cp "$$src/trip.md" "$$dst/trip.md" 2>/dev/null || true; \
@@ -123,15 +182,15 @@ demo-load:
done; cd /var/www/html && php bin/grav clearcache'
demo-reset:
docker exec intotheeast_grav bash -c 'for src in /var/www/html/user/docs/demo/trips/*/; do \
docker exec $(GRAV_CONTAINER) bash -c 'for src in /var/www/html/user/docs/demo/trips/*/; do \
rm -rf /var/www/html/user/pages/01.trips/$$(basename "$$src"); \
done; cd /var/www/html && php bin/grav clearcache'
pixelfed-import:
docker exec intotheeast_grav bash -c "which python3 || apt-get install -y python3 --no-install-recommends -q"
docker cp /home/mischa/Nextcloud/Downloads/pixelfed/pixelfed-statuses.json intotheeast_grav:/tmp/pixelfed-statuses.json
docker cp scripts/pixelfed-import.py intotheeast_grav:/tmp/pixelfed-import.py
docker exec -w /var/www/html intotheeast_grav python3 /tmp/pixelfed-import.py
docker exec $(GRAV_CONTAINER) bash -c "which python3 || apt-get install -y python3 --no-install-recommends -q"
docker cp /home/mischa/Nextcloud/Downloads/pixelfed/pixelfed-statuses.json $(GRAV_CONTAINER):/tmp/pixelfed-statuses.json
docker cp scripts/pixelfed-import.py $(GRAV_CONTAINER):/tmp/pixelfed-import.py
docker exec -w /var/www/html $(GRAV_CONTAINER) python3 /tmp/pixelfed-import.py
# ── Content sync (user repo ↔ Gitea) ──────────────────────────────────────────