Files
intotheeast-com/Makefile
T
m038andClaude Opus 4.8 c4891d8f60 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
2026-07-08 10:56:07 +02:00

346 lines
18 KiB
Makefile

# Local/shared config — always loaded. Keep remote credentials OUT of here;
# those live in .env.test / .env.prod. (docker compose also reads .env directly
# 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.
ENV ?=
-include .env.$(ENV)
export
REMOTE_PORT ?= 22
SSH := ssh -p $(REMOTE_PORT) $(REMOTE_USER)@$(REMOTE_HOST)
WEBROOT ?= $(REMOTE_HOME)/public_html
SITE_CONFIG_DIR ?= $(REMOTE_HOME)/site-config
# Hostname Grav uses to pick its per-environment config (user/env/<host>/).
# Defaults to the SSH host; override in .env.<ENV> only if the web hostname
# Grav sees differs from the SSH host (e.g. an addon domain on a shared box).
WEB_HOST ?= $(REMOTE_HOST)
# ── Environment guard + generated per-env remote targets ──────────────────────
# Every remote-* target below gains `-test` / `-prod` variants, e.g.
# make remote-install-prod → runs remote-install with ENV=prod
# Calling a bare remote target (no ENV) fails via guard-env.
REMOTE_TARGETS := remote-env-setup remote-env-remove remote-wipe remote-install \
remote-fetch remote-fetch-content remote-install-plugins remote-update-plugins \
remote-upgrade-grav remote-git-sync-disable remote-git-sync-enable \
remote-content-status remote-clean remote-diag remote-apply-env \
remote-seed-api-salt remote-secrets-audit \
remote-gpm-install remote-maintenance-on remote-maintenance-off \
remote-apply-plugin-patches
ENVS := test prod
guard-env:
@test -n "$(ENV)" || { echo "ERROR: no environment. Use an env-suffixed target, e.g. 'make remote-install-prod'."; exit 1; }
@test -f ".env.$(ENV)" || { echo "ERROR: missing .env.$(ENV)"; exit 1; }
define make-env-target
$(1)-$(2): ; @$$(MAKE) --no-print-directory $(1) ENV=$(2)
endef
$(foreach t,$(REMOTE_TARGETS),$(foreach e,$(ENVS),$(eval $(call make-env-target,$(t),$(e)))))
# ── Tests ─────────────────────────────────────────────────────────────────────
# Local test account — auto-created, never committed (see user/.gitignore).
# Keep the password free of shell/Make/URL-special chars so every consumer agrees.
GRAV_TEST_USER ?= testrunner
GRAV_TEST_PASS ?= Testpass1234
test-account:
@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'
test-config:
@bash scripts/test-form-config.sh
test-post: test-account
@bash scripts/test-post.sh
test-ui: test-account
@npx playwright test
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
build-assets:
docker run --rm \
-v $(PWD)/user/themes/intotheeast:/app \
-w /app node:20-alpine \
sh -c "npm install && npm run build"
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 $(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 $(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
# AFTER install-plugins (which overwrites them). See deploy/patches/README.md.
apply-plugin-patches:
@for p in deploy/patches/*.patch; do \
[ -f "$$p" ] || continue; \
if git apply --check "$$p" >/dev/null 2>&1; then \
git apply "$$p" && echo "applied $$p"; \
else \
echo "skipped $$p (already applied or does not match)"; \
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 $(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; \
cp "$$src/stories.md" "$$dst/04.stories/stories.md" 2>/dev/null || true; \
cp -r "$$src/04.stories/." "$$dst/04.stories/" 2>/dev/null || true; \
cp -r "$$src/dailies/." "$$dst/01.dailies/" 2>/dev/null || true; \
cp "$$src"/*.gpx "$$dst/" 2>/dev/null || true; \
chown -R 1000:1000 "$$dst"; \
done; cd /var/www/html && php bin/grav clearcache'
demo-reset:
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 $(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) ──────────────────────────────────────────
content-push:
git -C user push origin main
content-pull:
git -C user pull origin main
# ── Remote credentials ─────────────────────────────────────────────────────────
remote-env-setup: guard-env
@$(SSH) "printf 'GITEA_HOST=%s\nGITEA_USER=%s\nGITEA_TOKEN=%s\n' \
'$(GITEA_HOST)' '$(GITEA_USER)' '$(GITEA_TOKEN)' > ~/.env-intotheeast && chmod 600 ~/.env-intotheeast"
@echo "Credentials written to server. Run 'make remote-env-remove' when done."
remote-env-remove: guard-env
@$(SSH) "rm -f ~/.env-intotheeast"
@echo "Credentials removed from server."
# ── Remote: initial install ────────────────────────────────────────────────────
remote-wipe: guard-env
$(SSH) "cd $(WEBROOT) && rm -rf assets backup bin cache images logs system tmp vendor webserver-configs index.php .htaccess CHANGELOG.md LICENSE.txt README.md"
remote-install: guard-env
$(SSH) "WEBROOT=$(WEBROOT) \
SITE_CONFIG_DIR=$(SITE_CONFIG_DIR) \
USER_REPO=$(USER_REPO) \
MAIN_REPO=$(MAIN_REPO) \
GRAV_VERSION=$(GRAV_VERSION) \
PLUGINS='$(shell cat plugins.txt | tr '\n' ' ')' \
GITEA_HOST=$(GITEA_HOST) \
GITEA_USER=$(GITEA_USER) \
GITEA_TOKEN=$(GITEA_TOKEN) \
bash -s" < scripts/server-install.sh
# ── Remote: ongoing maintenance ────────────────────────────────────────────────
remote-fetch: guard-env
$(SSH) "git -C $(SITE_CONFIG_DIR) checkout main && git -C $(SITE_CONFIG_DIR) pull"
remote-fetch-content: guard-env
$(SSH) "git -C $(WEBROOT)/user fetch origin main && git -C $(WEBROOT)/user sparse-checkout disable && git -C $(WEBROOT)/user reset --hard origin/main"
remote-install-plugins: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm index -f && php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y"
$(MAKE) remote-apply-plugin-patches
remote-update-plugins: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm update -y && php bin/grav cache"
$(MAKE) remote-apply-plugin-patches
# Re-apply local fixes to git-ignored, GPM-managed third-party plugins on the
# remote (pristine after a GPM install/update). Piped over SSH like the git-sync
# scripts — no scp. `--forward` makes it a no-op when already applied. Runs
# automatically after remote-install-plugins / remote-update-plugins; safe to run
# standalone. See deploy/patches/README.md.
remote-apply-plugin-patches: guard-env
@for p in deploy/patches/*.patch; do \
[ -f "$$p" ] || continue; \
echo "remote-apply $$p"; \
$(SSH) "cd $(WEBROOT) && patch -p1 --forward -r - --no-backup-if-mismatch" < "$$p" || echo " (already applied or no-op)"; \
done
$(SSH) "cd $(WEBROOT) && php bin/grav clearcache"
remote-upgrade-grav: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm self-upgrade -y && php bin/grav cache"
remote-git-sync-disable: guard-env
$(SSH) "bash -s -- '$(WEBROOT)' false" < scripts/git-sync-toggle.sh
remote-git-sync-enable: guard-env
$(SSH) "bash -s -- '$(WEBROOT)' true" < scripts/git-sync-toggle.sh
remote-content-status: guard-env
$(SSH) "cd $(WEBROOT)/user && echo '--- HEAD ---' && git log -1 --oneline && echo '--- working tree ---' && git status --short && echo '--- config diff ---' && git diff -- config/ && echo '--- .gitignore diff ---' && git diff -- .gitignore"
remote-clean: guard-env
$(SSH) "cd $(WEBROOT) && php bin/grav clearcache"
# Install a single GPM package on the server (e.g. git-sync, which is
# intentionally NOT in plugins.txt — it is remote-only).
# Usage: make remote-gpm-install-prod PKG=git-sync
remote-gpm-install: guard-env
@test -n "$(PKG)" || { echo "ERROR: set PKG=<plugin-slug>"; exit 1; }
$(SSH) "cd $(WEBROOT) && php bin/gpm index -f && php bin/gpm install $(PKG) -y && php bin/grav clearcache"
# Deploy per-environment Grav config overrides to the server's
# user/env/<WEB_HOST>/config/ tree (deep-merged over the committed config).
# Source of truth: deploy/env/$(ENV)/system.yaml (version-controlled). This
# tree is outside the content repo, so it is NOT restored by content sync —
# re-run after any fresh install.
remote-apply-env: guard-env
@test -f deploy/env/$(ENV)/system.yaml || { echo "ERROR: missing deploy/env/$(ENV)/system.yaml"; exit 1; }
@host="$${WEB_HOST:-$(REMOTE_HOST)}"; \
test -n "$$host" || { echo "ERROR: WEB_HOST/REMOTE_HOST unresolved"; exit 1; }; \
$(SSH) "mkdir -p $(WEBROOT)/user/env/$$host/config && cat > $(WEBROOT)/user/env/$$host/config/system.yaml && cd $(WEBROOT) && php bin/grav clearcache" < deploy/env/$(ENV)/system.yaml; \
echo "Applied deploy/env/$(ENV)/system.yaml -> $(WEBROOT)/user/env/$$host/config/system.yaml"
# Seed a per-host popularity salt into the env override tree so the api plugin
# reads it there instead of appending one to the git-tracked config/plugins/
# api.yaml. That appended salt kept the content working tree dirty, which broke
# git-sync's auto-merge on webhook. Salt is generated server-side and never
# committed (a committed salt would be globally known). Idempotent: an existing
# salt is kept, so re-running never rotates it.
remote-seed-api-salt: guard-env
@host="$${WEB_HOST:-$(REMOTE_HOST)}"; \
test -n "$$host" || { echo "ERROR: WEB_HOST/REMOTE_HOST unresolved"; exit 1; }; \
$(SSH) "set -e; \
envfile=$(WEBROOT)/user/env/$$host/config/plugins/api.yaml; \
mkdir -p \$$(dirname \"\$$envfile\"); \
if grep -qE '^[[:space:]]*salt:' \"\$$envfile\" 2>/dev/null; then \
echo \"salt already present in \$$envfile — keeping it\"; \
else \
salt=\$$(openssl rand -hex 32); \
printf 'popularity:\n salt: %s\n' \"\$$salt\" > \"\$$envfile\"; \
echo \"seeded new per-host salt into \$$envfile\"; \
fi; \
git -C $(WEBROOT)/user checkout -- config/plugins/api.yaml 2>/dev/null || true; \
cd $(WEBROOT) && php bin/grav clearcache >/dev/null 2>&1 || true; \
echo '--- base api.yaml status (expect clean) ---'; \
git -C $(WEBROOT)/user status --short config/plugins/api.yaml; \
echo '(if the line above is empty, the tree is clean)'"
# Read-only health check: plugin install state, versions, key config, log tail.
remote-diag: guard-env
$(SSH) "cd $(WEBROOT) && \
echo '=== Grav version ==='; php bin/grav --version 2>/dev/null; \
echo '=== installed plugin versions ==='; for p in login admin2 flex-objects form api; do printf '%s: ' \"\$$p\"; grep -m1 '^version:' user/plugins/\$$p/blueprints.yaml 2>/dev/null || echo '(NOT installed)'; done; \
echo '=== what does GPM say about api? ==='; php bin/gpm info api 2>&1 | head -12; \
echo '=== api override (enabled/route/session) ==='; grep -nE '^enabled:|^route:|session_enabled:' user/config/plugins/api.yaml 2>&1; \
echo '=== per-env override present? ==='; for f in user/env/*/config/system.yaml; do echo \"\$$f:\"; cat \"\$$f\" 2>/dev/null | grep -E 'cache:|debug:|auto_reload:'; done; \
echo '=== twig cache populating? (non-empty => cache on) ==='; ls cache/twig/ 2>/dev/null | head -1 || echo '(empty)'; \
echo '=== git-sync config (secrets redacted) ==='; grep -vaiE 'password|token|secret' user/config/plugins/git-sync.yaml user/env/*/config/plugins/git-sync.yaml 2>/dev/null; \
echo '=== grav.log tail ==='; tail -8 logs/grav.log 2>/dev/null"
# Secret-safe audit: lists WHERE per-host secret/config files live (config/ vs
# env/<host>/config/) and their sizes — never prints contents. Used to decide
# whether a `reset --hard` would clobber a live runtime secret.
remote-secrets-audit: guard-env
$(SSH) "cd $(WEBROOT)/user && \
echo '=== tracked in git? (git ls-files) ==='; git ls-files config/security-private.php config/security.yaml config/versions.yaml config/plugins/api-private.php config/plugins/git-sync.yaml; \
echo '=== config/ copies (size only) ==='; ls -la config/security.yaml config/security-private.php config/versions.yaml config/plugins/api-private.php config/plugins/git-sync.yaml 2>&1; \
echo '=== env/<host>/config copies (size only) ==='; ls -la env/*/config/security.yaml env/*/config/security-private.php env/*/config/plugins/api-private.php env/*/config/plugins/git-sync.yaml 2>&1; \
echo '=== does security.yaml reference the private php? (key names only) ==='; grep -aoE '^[a-z_]+:' config/security.yaml 2>/dev/null; for f in env/*/config/security.yaml; do echo \"\$$f:\"; grep -aoE '^[a-z_]+:' \"\$$f\" 2>/dev/null; done; true"
remote-maintenance-on: guard-env
$(SSH) "bash -s on $(WEBROOT)" < scripts/server-maintenance.sh
remote-maintenance-off: guard-env
$(SSH) "bash -s off $(WEBROOT)" < scripts/server-maintenance.sh