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
+2
View File
@@ -2,6 +2,8 @@
.env .env
.env.prod .env.prod
.env.test .env.test
# Per-worktree dev-server identity, written by `make worktree-new`
.worktree-env
# Grav CMS # Grav CMS
/user/ /user/
+1 -1
View File
@@ -155,7 +155,7 @@ Only these folders are tracked in the `user/` Git repo: `pages/`, `config/`, `ac
- **Two repos, two cadences.** Outer repo = dev environment (tests/docs/scripts/Docker). `user/` = content + theme, with its own remote and `make content-push` cadence. The outer repo pins an exact `user/` commit via the `user` gitlink. - **Two repos, two cadences.** Outer repo = dev environment (tests/docs/scripts/Docker). `user/` = content + theme, with its own remote and `make content-push` cadence. The outer repo pins an exact `user/` commit via the `user` gitlink.
- **Pointer-bump convention.** Routine content changes → **do not** bump the pin (leave it stale; harmless). At the **end of a cross-repo feature** → bump the pin once to the finished `user/` commit. Pin a commit reachable from `user/`'s published `main` (prefer the merge-to-main commit, not a squash-away branch tip), and **push `user/` before the outer repo** (superproject references a child SHA that must already exist upstream). The pin is dev-side coordination only — production pulls `user/` via the content webhook independently. - **Pointer-bump convention.** Routine content changes → **do not** bump the pin (leave it stale; harmless). At the **end of a cross-repo feature** → bump the pin once to the finished `user/` commit. Pin a commit reachable from `user/`'s published `main` (prefer the merge-to-main commit, not a squash-away branch tip), and **push `user/` before the outer repo** (superproject references a child SHA that must already exist upstream). The pin is dev-side coordination only — production pulls `user/` via the content webhook independently.
- **`M user` / `m user` is normal.** `M` = pin differs from `user/` HEAD (bump pending/intentional). `m` = submodule working tree dirty (e.g. local-testing `config/site.yaml`). Neither is an error — do not "fix" them by committing the gitlink or the `site.yaml`. - **`M user` / `m user` is normal.** `M` = pin differs from `user/` HEAD (bump pending/intentional). `m` = submodule working tree dirty (e.g. local-testing `config/site.yaml`). Neither is an error — do not "fix" them by committing the gitlink or the `site.yaml`.
- **Worktrees for parallel work.** A worktree off `main` gets its own `user/` (`git submodule update --init user`) and can run its own dev server (`docker compose -p itte-<feature> up` — the `./user` mount is relative, so each worktree serves its own content). Tooling worktrees live under `.worktrees/` (excluded via `.git/info/exclude`). To add a commit to `main` while the main checkout is on another branch, use a throwaway `main` worktree rather than `git checkout main`. - **Worktrees for parallel work — use the make targets, don't do it by hand.** `make worktree-new NAME=<feature>` (from the main checkout) creates the outer worktree off `main`, initialises its own `user/` submodule, branches both, and starts an **isolated** dev server (own container name + auto-assigned port `8090+`, persisted in a git-ignored `.worktree-env` so every `make`/compose command in that worktree targets its own server). `make worktree-rm NAME=<feature>` tears it down cleanly (compose down → `submodule deinit``worktree remove``prune`) — skipping the deinit is what leaves orphaned `.worktrees/` dirs. Worktrees live under `.worktrees/` (excluded via `.git/info/exclude`). A fresh worktree's `user/` is empty until the submodule init runs, and `M user`/`m user` is normal (see above) — do not "fix" either. To add a commit to `main` while the main checkout is on another branch, use a throwaway `main` worktree rather than `git checkout main`.
## 1. Environment modes ## 1. Environment modes
+70 -11
View File
@@ -3,6 +3,12 @@
# for ${UID}/${GID} substitution and the travel-memories env_file.) # for ${UID}/${GID} substitution and the travel-memories env_file.)
-include .env -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 # Remote config — loaded only when targeting an environment. ENV is set
# automatically by the env-suffixed remote targets (e.g. `make remote-install-prod`); # 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. # each .env.<ENV> holds a full, self-contained set of remote vars.
@@ -49,7 +55,7 @@ GRAV_TEST_USER ?= testrunner
GRAV_TEST_PASS ?= Testpass1234 GRAV_TEST_PASS ?= Testpass1234
test-account: 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)" \ || 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' -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 ────────────────────────────────────────────────────────────────── # ── 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: build:
docker compose build docker compose build
@@ -78,19 +91,24 @@ build-assets:
start: start:
docker compose up -d 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: stop:
docker compose down docker compose down
setup: build start install-plugins fix-perms setup: build start install-plugins fix-perms
fix-perms: fix-perms:
docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser" docker exec $(GRAV_CONTAINER) 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 $(GRAV_CONTAINER) chown -R 1000:1000 /var/www/html
docker exec intotheeast_grav apachectl graceful docker exec $(GRAV_CONTAINER) apachectl graceful
install-plugins: 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 $(MAKE) apply-plugin-patches
# Re-apply local fixes to git-ignored, GPM-managed third-party plugins. Run this # Re-apply local fixes to git-ignored, GPM-managed third-party plugins. Run this
@@ -105,13 +123,54 @@ apply-plugin-patches:
fi; \ fi; \
done 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 content ──────────────────────────────────────────────────────────────
demo-load: demo-load:
# Load every fixture trip under docs/demo/trips/ into the pages tree. # Load every fixture trip under docs/demo/trips/ into the pages tree.
# Source uses dailies/ + 04.stories/; dailies/ maps to 01.dailies/ on copy. # 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. # 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; \ slug=$$(basename "$$src"); dst=/var/www/html/user/pages/01.trips/$$slug; \
mkdir -p "$$dst/01.dailies" "$$dst/04.stories"; \ mkdir -p "$$dst/01.dailies" "$$dst/04.stories"; \
cp "$$src/trip.md" "$$dst/trip.md" 2>/dev/null || true; \ 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' done; cd /var/www/html && php bin/grav clearcache'
demo-reset: 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"); \ rm -rf /var/www/html/user/pages/01.trips/$$(basename "$$src"); \
done; cd /var/www/html && php bin/grav clearcache' done; cd /var/www/html && php bin/grav clearcache'
pixelfed-import: pixelfed-import:
docker exec intotheeast_grav bash -c "which python3 || apt-get install -y python3 --no-install-recommends -q" 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 intotheeast_grav:/tmp/pixelfed-statuses.json docker cp /home/mischa/Nextcloud/Downloads/pixelfed/pixelfed-statuses.json $(GRAV_CONTAINER):/tmp/pixelfed-statuses.json
docker cp scripts/pixelfed-import.py intotheeast_grav:/tmp/pixelfed-import.py docker cp scripts/pixelfed-import.py $(GRAV_CONTAINER):/tmp/pixelfed-import.py
docker exec -w /var/www/html intotheeast_grav python3 /tmp/pixelfed-import.py docker exec -w /var/www/html $(GRAV_CONTAINER) python3 /tmp/pixelfed-import.py
# ── Content sync (user repo ↔ Gitea) ────────────────────────────────────────── # ── Content sync (user repo ↔ Gitea) ──────────────────────────────────────────
+5 -3
View File
@@ -1,13 +1,15 @@
services: services:
grav: grav:
build: . build: .
container_name: intotheeast_grav # Overridable so a git worktree can run its own isolated dev server (see
# `make worktree-new`); unset → the canonical main-checkout values below.
container_name: ${GRAV_CONTAINER:-intotheeast_grav}
environment: environment:
- GRAV_CHANNEL=production - GRAV_CHANNEL=production
- APACHE_RUN_USER=#1000 - APACHE_RUN_USER=#1000
- APACHE_RUN_GROUP=#1000 - APACHE_RUN_GROUP=#1000
ports: ports:
- "8081:80" - "${GRAV_PORT:-8081}:80"
volumes: volumes:
- ./user:/var/www/html/user - ./user:/var/www/html/user
- ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini - ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini
@@ -16,7 +18,7 @@ services:
travel-memories: travel-memories:
build: ./services/travel-memories build: ./services/travel-memories
ports: ports:
- "8082:8082" - "${TM_PORT:-8082}:8082"
volumes: volumes:
- ./docs/immich-workflow:/app/state - ./docs/immich-workflow:/app/state
- ./user/pages:/app/pages - ./user/pages:/app/pages