A ui-test entry had survived into the active trip's dailies. Three independent failures had to line up for that, and all three were real: 1. cleanupEntry() used host-side fs.rmSync. Grav's Apache workers run as root, so every entry the form creates is root-owned and recursive removal needs write permission on that directory — which the host user lacks. Cleanup had never worked for form-created entries; it just threw inside a path nothing checked. It now falls back to `docker exec … rm -rf` in the container that actually serves USER_DIR. 2. globalTeardown's dailies sweep keyed off a `parent:` in post-form.md — a key deliberately removed (the write target comes from site.yaml active_trip, and CLAUDE.md forbids re-adding a static parent). The regex could never match, so dailiesDir was always null and the sweep silently did nothing. It now reuses helpers' own resolution instead of keeping a divergent copy. 3. Nothing pinned the suite to this checkout's server. playwright.config.js defaults to :8081, so a worktree run hit the MAIN checkout — entries created in one content tree while the specs asserted and cleaned up in another. test-ui now passes GRAV_BASE_URL from GRAV_PORT, and globalSetup hard-fails when the server's bind mount disagrees with the tree the specs read. Also fixed, found on the way to a green run: - test-account interpolated the password into an `sh -c` string, so a password containing a shell metacharacter was re-parsed by the container's shell (`sh: 2: <fragment>: not found`, no account, every UI run dead). It now travels via `docker exec -e`, making the recipe indifferent to its contents. - `make start` in a worktree always failed: travel-memories declares `env_file: .env` and worktree-new creates none. It degrades to start-grav there — a worktree with no server is what sent runs to :8081 in the first place. - test-form-config asserted a hero_image field that 8cf1145 deliberately removed; it had been failing ever since. Verified: config 22/22, post 6/6, location-override 20/20, and a full UI run now leaves zero ui-test entries behind. The remaining UI failures are pre-existing on main — site.yaml pins owner_username to a real account while the suite logs in as testrunner, so owner-only controls never render for it. Only trip-publish.spec.js patches that; delete-flow, edit-mode and anon-view do not. Left for a separate branch.
412 lines
22 KiB
Makefile
412 lines
22 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-warmup 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
|
|
|
|
# The password is handed to the container through `docker exec -e` (the bare
|
|
# form, which forwards the already-exported variable) rather than interpolated
|
|
# into the `sh -c` string. Interpolating it meant any shell-special character in
|
|
# GRAV_TEST_PASS was re-parsed by the container's shell — a `.env` password
|
|
# containing one produced `sh: 2: <fragment>: not found` and no test account.
|
|
# The recipe is now indifferent to the password's contents.
|
|
test-account:
|
|
@docker exec -e GRAV_TEST_PASS $(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
|
|
|
|
# Pinned to THIS checkout's port, not playwright.config.js's :8081 default. In a
|
|
# worktree that default silently pointed the suite at the main checkout's server,
|
|
# so entries were created in main's user/ while the specs asserted and cleaned up
|
|
# in the worktree's — leaving ui-test entries behind in real trip content.
|
|
# tests/global-setup.js now also hard-fails on that mismatch.
|
|
GRAV_BASE_URL ?= http://localhost:$(GRAV_PORT)
|
|
|
|
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
|
|
|
|
# The container boots as root (the base image entrypoint needs it to bind :80
|
|
# and set up cron), so a bare `docker exec` runs as root and any file it writes
|
|
# into the ./user bind mount is root-owned on the host. Run the file-CREATING
|
|
# CLI commands as the host user instead, so their output belongs to you.
|
|
HOST_UID := $(shell id -u)
|
|
HOST_GID := $(shell id -g)
|
|
|
|
build:
|
|
docker compose build
|
|
|
|
build-assets:
|
|
# --user: outputs (node_modules, js/ bundles, css-compiled/) land in the
|
|
# tracked theme tree owned by the host user, not root. HOME=/tmp gives npm
|
|
# a writable cache when running as a non-root uid.
|
|
docker run --rm --user $(HOST_UID):$(HOST_GID) -e HOME=/tmp \
|
|
-v $(PWD)/user/themes/intotheeast:/app \
|
|
-w /app node:20-alpine \
|
|
sh -c "npm install && npm run build"
|
|
|
|
# In a worktree this degrades to start-grav. The travel-memories service declares
|
|
# `env_file: .env`, and worktree-new does not create a .env, so a plain
|
|
# `docker compose up -d` there dies with "env file ... not found" — leaving the
|
|
# worktree with no server at all, which is how test runs ended up silently
|
|
# targeting the main checkout.
|
|
start:
|
|
@if [ -f .worktree-env ]; then \
|
|
echo "→ worktree: starting the grav service only (travel-memories needs a .env, which worktrees have none)"; \
|
|
docker compose up -d grav; \
|
|
else \
|
|
docker compose up -d; \
|
|
fi
|
|
|
|
# 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:
|
|
# cache/ and tmp/ are root-owned in the image, so make them writable first
|
|
# (container-internal chown — never touches the host) so gpm can run AS YOU.
|
|
docker exec $(GRAV_CONTAINER) chown -R $(HOST_UID):$(HOST_GID) /var/www/html/cache /var/www/html/tmp
|
|
# gpm runs as the host user, so the plugins it writes into ./user/plugins are
|
|
# owned by you, not root — no post-hoc chown, no root files to clean up later.
|
|
docker exec -u $(HOST_UID):$(HOST_GID) -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.
|
|
#
|
|
# ⚠️ A fixture whose folder name matches a REAL trip's slug is copied straight
|
|
# over that live page — docs/demo/trips/italy-2025/ collides with the real
|
|
# italy-2025 trip on purpose (the fixture supplies its GPX + dailies). So any
|
|
# field the fixture's trip.md omits gets silently deleted from real content on
|
|
# every test run: it had been dropping the trip's tagline that way. Keep a
|
|
# colliding fixture's trip.md byte-identical to the live page.
|
|
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"
|
|
|
|
# Post-deploy cache refresh: clear, then WARM. A `reset --hard` content deploy
|
|
# leaves Grav's compiled-Twig/page cache stale, and the first real visitor pays
|
|
# the recompile cost — so clear it and pre-render the public pages ourselves.
|
|
# Grav has no native warmup command, so this is an HTTP crawl of the live site:
|
|
# homepage + trips listing + every trip page linked from it (no sitemap plugin
|
|
# installed, so we scrape the listing instead of /sitemap.xml). The crawl runs
|
|
# from here over public HTTPS, so it also doubles as a smoke test — a non-200 on
|
|
# `/` is surfaced loudly. Run after every content deploy: `make remote-warmup-prod`.
|
|
remote-warmup: guard-env
|
|
$(SSH) "cd $(WEBROOT) && php bin/grav clearcache" >/dev/null
|
|
@base="https://$${WEB_HOST:-$(REMOTE_HOST)}"; \
|
|
echo "warming $$base (clear done) ..."; \
|
|
trip_urls=$$(curl -s "$$base/trips" | grep -oE '/trips/[a-z0-9][a-z0-9-]*' | sort -u); \
|
|
fail=0; \
|
|
for u in / /trips $$trip_urls; do \
|
|
code=$$(curl -s -o /dev/null -w '%{http_code}' "$$base$$u"); \
|
|
printf ' %-40s %s\n' "$$u" "$$code"; \
|
|
case "$$code" in 2*|3*) ;; *) fail=1;; esac; \
|
|
done; \
|
|
if [ "$$fail" = 1 ]; then echo "WARNING: one or more pages returned a non-2xx/3xx status"; else echo "warmup OK — all pages 2xx/3xx"; fi
|
|
|
|
# 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
|