# 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 # 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. 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//). # Defaults to the SSH host; override in .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 intotheeast_grav 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 ────────────────────────────────────────────────────────────────── 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 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 install-plugins: docker exec -w /var/www/html intotheeast_grav 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 # ── 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 \ 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 intotheeast_grav 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 # ── 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="; 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//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//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//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