Test env upgraded to Grav 2.0.7, plugins to stable, smoke-tested, git-sync re-enabled. Documents the strict_mode Twig 3 migration gotcha (fresh-image local build skips the schema migration that self-upgrade runs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
24 KiB
Grav 2.0.4 Upgrade + GPM-Manage Plugins — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Status: ✅ Complete (2026-07-04) — Phase 1 (local) and Phase 2 (remote test env) both validated and shipped; git-sync re-enabled on test. Phase 3 (prod) is documentation-only per design. See "Known issue" below re: Form 9.1.10 filepond.
Goal: Upgrade Grav core 2.0.0-rc.10 → 2.0.4 stable and promote admin2/api/flex-objects to GPM management, validated on local then the remote test env (prod is documented-only).
Architecture: One dependency-forced atomic upgrade. Local core is baked into the Docker image (rebuild); the server upgrades in place via bin/gpm self-upgrade + bin/gpm update. The GPM release channel is switched from testing to stable in user/config/system.yaml. git-sync is disabled for the duration of the remote upgrade and left off pending user validation.
Tech Stack: Grav 2.0 (PHP 8.3), GPM CLI, Docker Compose, Make (env-suffixed remote targets), Gitea content sync.
Spec: docs/working/specs/2026-07-04-grav-2.0.4-upgrade-design.md
Global Constraints
- Version floors (GPM enforces):
grav >= 2.0.4,api >= 1.0.6,admin2 >= 2.0.9,flex-objects >= 1.4.3,login >= 3.8.11,form >= 6.0.0. Assert with>=, not==— GPM may serve a newer stable patch at execution time. - Only write inside
travel-blog-intotheeast/or its subfolders. - Dual git repos. The project root is one repo;
user/is a separate repo where onlypages/ config/ accounts/ themes/are tracked (plugins/is gitignored exceptcache-on-save/andstory-blocks/). Changes touser/config/system.yamlcommit to the user repo and reach the server viamake content-push→ server pull; everything else commits to the root repo. - GPM channel authority is
user/config/system.yaml→gpm.releases(must bestableon the server before any GPM op).GRAV_CHANNELin docker-compose is cosmetic/consistency only. - Never read
.env*. Usemake remote-*targets for all server ops. - Do not touch
twig.cache(staysfalsein dev per CLAUDE.md). git-syncis remote-only: never add it toplugins.txt; disable it during the remote upgrade and leave it disabled until the user re-enables.- Prod is empty → Phase 3 is documentation only, never executed.
- Verified CLI names (against the rc.10 container):
php bin/gpm self-upgrade -y(core),php bin/gpm update -y(all plugins),php bin/grav cache(clear cache).bin/grav upgradedoes not exist.
File structure
| File | Repo | Responsibility |
|---|---|---|
Dockerfile |
root | Baked local core version (grav-admin zip URL) |
plugins.txt |
root | GPM plugin manifest — gains admin2/api/flex-objects |
docker-compose.yml |
root | GRAV_CHANNEL cosmetic bump |
user/config/system.yaml |
user | Authoritative GPM channel (gpm.releases) |
scripts/server-install.sh |
root | Fresh-install script — drop admin2/api special-casing |
scripts/git-sync-toggle.sh |
root | New: idempotently set git-sync enabled: on the server |
Makefile |
root | Fix remote-upgrade-grav; add 3 remote targets |
docs/working/plans/... CLAUDE.md docs/reference/architecture.md |
root | Runbook + stack docs |
Task 1: Phase 0 — core, plugin-list, and channel edits
Files:
- Modify:
Dockerfile(the grav-admin zip URL line) - Modify:
plugins.txt - Modify:
docker-compose.yml(GRAV_CHANNEL) - Modify:
user/config/system.yaml(gpm.releases)
Interfaces:
-
Produces: local image that installs Grav 2.0.4;
plugins.txtcontainingapi,admin2,flex-objects;stableGPM channel consumed by Task 2 (local) and Task 5 (server). -
Step 1: Bump the core version in the Dockerfile
In Dockerfile, change the download URL:
RUN curl -sL 'https://github.com/getgrav/grav/releases/download/2.0.4/grav-admin-v2.0.4.zip' \
-o /tmp/grav-admin.zip \
(Only the URL changes — the zip still extracts to /tmp/grav-admin/, so every cp line below it is unchanged.)
- Step 2: Add the three plugins to
plugins.txt
Append these lines to plugins.txt (order is not significant; GPM resolves deps):
api
admin2
flex-objects
- Step 3: Switch the GPM channel to stable
In user/config/system.yaml, under the gpm: block (currently line ~212):
gpm:
releases: stable
official_gpm_only: true
(Change testing → stable. Leave official_gpm_only as-is.)
- Step 4: Bump the cosmetic channel env
In docker-compose.yml, under the grav service environment:
- GRAV_CHANNEL=production
- Step 5: Commit the user-repo change
cd user
git add config/system.yaml
git commit -m "config: switch GPM release channel testing -> stable"
cd ..
Expected: commit succeeds in the user repo.
- Step 6: Commit the root-repo changes
git add Dockerfile plugins.txt docker-compose.yml
git commit -m "build: pin Grav core 2.0.4 and add admin2/api/flex-objects to plugins.txt"
Expected: commit succeeds on branch grav-2.0.4-upgrade.
Task 2: Phase 1 — local build, clean install, validation
Files:
- No file edits. Executes the Task 1 changes locally.
Interfaces:
-
Consumes: Task 1 (Dockerfile 2.0.4, plugins.txt, stable channel).
-
Produces: a proven-working local 2.0.4 stack — the go/no-go gate for the remote phases.
-
Step 1: Remove the stale manually-extracted plugin folders
These were hand-extracted from the rc.10 bundle; GPM must install them fresh.
rm -rf user/plugins/admin2 user/plugins/api user/plugins/flex-objects
Expected: the three folders are gone (ls user/plugins/ no longer lists them). They are gitignored, so git status in user/ is unaffected.
- Step 2: Rebuild the image with core 2.0.4
make build
Expected: build completes; the RUN layer downloads grav-admin-v2.0.4.zip.
- Step 3: Recreate the container
make start
Expected: intotheeast_grav is up on http://localhost:8081.
- Step 4: Confirm the core version is 2.0.4
docker exec intotheeast_grav php bin/grav --version
Expected output contains: Grav CLI Application 2.0.4 (or a newer 2.0.x).
- Step 5: Update already-installed GPM plugins to stable
This bumps login (3.8.9 → ≥3.8.11, required by api) and form before the new plugins install. gpm install alone would skip them because they already exist.
docker exec -w /var/www/html intotheeast_grav php bin/gpm update -y
Expected: login, form, shortcode-core, etc. report as updated (or already up to date).
- Step 6: Install the newly-listed plugins
make install-plugins
Expected: admin2, api, flex-objects install; their dependencies resolve against the now-current login/form; no "requires grav >= 2.0.4" errors.
- Step 7: Clear the cache
docker exec intotheeast_grav php bin/grav cache
Expected: "Cache cleared" output.
- Step 8: Assert plugin versions meet the floors
docker exec intotheeast_grav sh -c 'cd /var/www/html && for p in admin2 api flex-objects login form; do printf "%s: " "$p"; grep -m1 "^version:" user/plugins/$p/blueprints.yaml; done'
Expected (at least): admin2: version: 2.0.9, api: version: 1.0.6, flex-objects: version: 1.4.3, login: version: 3.8.11, form: version: 9.1.8 — equal or higher.
- Step 9: Run the automated smoke suite
This exercises the posting pipeline (test-post submits via the real form → add-page-by-form → cache-on-save) and renders pages via Playwright — the exact admin2/api-critical path.
make test
Expected: test-config, test-post, and test-ui all pass.
- Step 10: Manual browser spot-check
Visit and confirm each renders without error:
- http://localhost:8081/ (home)
- the active trip page (
/trips/japan-korea-2026) — filter bar + map load - one story page — hero + shortcodes render
- http://localhost:8081/admin2 — login page loads; log in
- http://localhost:8081/gpx-manager — list loads; upload a small
.gpx, then delete it
Expected: all load; no PHP errors in docker logs intotheeast_grav.
- Step 11: Checkpoint (no commit needed)
No files changed in this task. If any step failed, stop and diagnose before proceeding — this is the go/no-go gate for remote work.
Task 3: Remote Makefile targets (fix + additions)
Files:
- Create:
scripts/git-sync-toggle.sh - Modify:
Makefile(fixremote-upgrade-grav; addremote-update-plugins,remote-git-sync-disable,remote-git-sync-enable; register the new targets inREMOTE_TARGETS)
Interfaces:
-
Produces:
make remote-upgrade-grav-<env>,make remote-update-plugins-<env>,make remote-git-sync-disable-<env>,make remote-git-sync-enable-<env>— consumed by Task 5. -
Step 1: Create the git-sync toggle script
Create scripts/git-sync-toggle.sh (piped to the server via bash -s, matching the server-install.sh pattern). It only ever rewrites the top-level enabled: key — never folders or the encrypted token.
#!/bin/bash
set -e
FILE="$1"
STATE="$2"
: "${FILE:?usage: git-sync-toggle.sh <git-sync.yaml path> <true|false>}"
: "${STATE:?usage: git-sync-toggle.sh <git-sync.yaml path> <true|false>}"
if [ ! -f "$FILE" ]; then
echo "ERROR: $FILE not found — is git-sync installed on this server?" >&2
exit 1
fi
if grep -qE '^enabled:' "$FILE"; then
sed -i -E "s/^enabled:.*/enabled: ${STATE}/" "$FILE"
else
printf 'enabled: %s\n' "$STATE" | cat - "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
fi
echo "git-sync now: $(grep -E '^enabled:' "$FILE")"
- Step 2: Make it executable
chmod +x scripts/git-sync-toggle.sh
- Step 3: Fix the broken
remote-upgrade-gravtarget
In Makefile, replace the body of remote-upgrade-grav (currently php bin/grav upgrade, which is not a real command):
remote-upgrade-grav: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm self-upgrade -y && php bin/grav cache"
- Step 4: Add the plugin-update target
Add below remote-install-plugins:
remote-update-plugins: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm update -y && php bin/grav cache"
- Step 5: Add the git-sync toggle targets
remote-git-sync-disable: guard-env
$(SSH) "bash -s -- '$(WEBROOT)/user/config/plugins/git-sync.yaml' false" < scripts/git-sync-toggle.sh
remote-git-sync-enable: guard-env
$(SSH) "bash -s -- '$(WEBROOT)/user/config/plugins/git-sync.yaml' true" < scripts/git-sync-toggle.sh
- Step 6: Add a server content-status target
For reviewing config drift after the plugin upgrade without raw SSH:
remote-content-status: guard-env
$(SSH) "cd $(WEBROOT)/user && git status --short && echo '--- config diff ---' && git diff -- config/"
- Step 7: Register the new targets for env-suffix generation
In Makefile, extend the REMOTE_TARGETS list so the -test/-prod variants get generated:
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-maintenance-on remote-maintenance-off
- Step 8: Verify the targets exist and expand correctly (dry run)
make -n remote-update-plugins-test
make -n remote-git-sync-disable-test
make -n remote-upgrade-grav-test
make -n remote-content-status-test
Expected: each prints the intended ssh ... command with ENV=test resolved, and no "No rule to make target" error. (No server is contacted by -n.)
- Step 9: Commit
git add scripts/git-sync-toggle.sh Makefile
git commit -m "build: fix remote-upgrade-grav; add remote plugin-update, git-sync toggle, content-status targets"
Task 4: Fresh-install script cleanup (option B server-side)
Files:
- Modify:
scripts/server-install.sh(remove admin2/api stash+restore)
Interfaces:
-
Consumes:
plugins.txtnow containing admin2/api/flex-objects (Task 1). -
Produces: a fresh-install path where admin2/api/flex install purely via
gpm install $PLUGINS. -
Step 1: Remove the zip-stash lines
In scripts/server-install.sh, delete lines 25–26 (the admin2/api stash into /tmp):
cp -rf grav-admin/user/plugins/admin2 /tmp/admin2-plugin
cp -rf grav-admin/user/plugins/api /tmp/api-plugin
- Step 2: Remove the restore lines
Delete lines 43–45 (the restore after the user re-clone):
cp -rf /tmp/admin2-plugin user/plugins/admin2
cp -rf /tmp/api-plugin user/plugins/api
rm -rf /tmp/admin2-plugin /tmp/api-plugin
Leave mkdir -p user/plugins user/accounts user/data in place. admin2/api/flex now come from php bin/gpm install $PLUGINS -y (unchanged line ~48).
- Step 3: Syntax-check the script
bash -n scripts/server-install.sh
Expected: no output (valid syntax).
- Step 4: Commit
git add scripts/server-install.sh
git commit -m "build: drop admin2/api zip-stash from server-install; install via GPM (option B)"
Task 5: Phase 2 — test env upgrade
Files:
- No file edits. Executes against the test environment using Task 3 targets.
Interfaces:
-
Consumes: Tasks 1–4 (pushed to Gitea), the
-testmake targets. -
Produces: test env on 2.0.4 with GPM-managed plugins, validated; git-sync left disabled.
-
Step 1: Push all changes to Gitea
The server pulls user/ content (incl. the stable-channel system.yaml) from Gitea; the root repo pushes normally.
git push origin grav-2.0.4-upgrade # or merge to the branch the server tracks, per your deploy convention
make content-push # pushes the user repo commit (system.yaml) to Gitea
Expected: both remotes updated. (Confirm with the user which branch the test server tracks before pushing.)
- Step 2: Disable git-sync on test
make remote-git-sync-disable-test
Expected: prints git-sync now: enabled: false.
- Step 3: Pull latest content to the test server
Brings the gpm.releases: stable change onto the server before any GPM op.
make remote-fetch-content-test
Expected: server user/ fast-forwards; user/config/system.yaml shows releases: stable.
- Step 4: Upgrade the core on test
make remote-upgrade-grav-test
Expected: bin/gpm self-upgrade moves core rc.10 → 2.0.4 (stable channel); cache cleared. If it fails on a shared-folder error (see spec Risks), retry is safe — self-upgrade supports -o/--overwrite; add it to the target temporarily if a retry is needed.
- Step 5: Update all plugins on test
make remote-update-plugins-test
Expected: admin2 → ≥2.0.9, api → ≥1.0.6, flex-objects → ≥1.4.3, login → ≥3.8.11, form, git-sync all update to their stable versions; cache cleared.
- Step 6: Review server config drift
Inspect the server user/ working tree for unexpected rewrites from the plugin upgrades (do NOT blind-commit):
make remote-content-status-test
Expected: review any config/ diffs deliberately. Discard server-specific/reformatting churn; keep only intended changes. (git-sync is disabled, so nothing auto-commits while you review.)
- Step 7: Smoke-test the test URL
Against the test site (URL per your test env), confirm:
- home, a trip page, a story render
- admin2 login works
- submit one
/post→ the entry appears in the active trip's dailies /gpx-managerlists, uploads, and deletes a file
Expected: all pass. (git-sync stays disabled, so the new post will not auto-sync yet — that's expected and verified in Step 9.)
- Step 8: Notify the user — validation checkpoint
Report results and explicitly state that git-sync remains disabled on test pending their validation. Do not re-enable automatically.
- Step 9: (User-gated) Re-enable git-sync and verify sync
After the user confirms validation:
make remote-git-sync-enable-test
make content-push # or trigger a content change; confirm it syncs through
Expected: git-sync now: enabled: true; a content round-trip syncs between the test server and Gitea.
Task 6: Docs, prod runbook, and memory
Files:
- Modify:
CLAUDE.md(stack versions + plugin-management model) - Modify:
docs/reference/architecture.md(versions/channel) - Modify:
docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md(this file — Phase 3 runbook + Status) - Modify: memory files under the auto-memory dir (project-grav2-upgrade, project-plugin-architecture)
Interfaces:
-
Consumes: the completed local + test upgrade.
-
Produces: current docs; an executable-but-unexecuted prod runbook.
-
Step 1: Update the stack facts in
CLAUDE.md
Change the "Current stack" block: Grav 2.0.4 (not rc.10); Admin2 to the installed stable version; note that admin2/api/flex-objects are now GPM-managed via plugins.txt (no longer hand-extracted); note gpm.releases: stable.
- Step 2: Update
docs/reference/architecture.md
Reflect core 2.0.4, stable channel, and the three-category plugin model (GPM-managed / former-manual-now-GPM / remote-only git-sync).
- Step 3: Write the Phase 3 prod runbook
Append a "Phase 3 — Production (fresh install, NOT executed)" section to this plan documenting: run make remote-install-prod with GRAV_VERSION=2.0.4; admin2/api/flex install via GPM from plugins.txt; then set up git-sync manually (install, add encrypted token, apply the folders: array fix per docs/working/git-sync-notes.md), and leave it disabled until first validation.
- Step 4: Update memory
Update project-grav2-upgrade.md (now on 2.0.4 stable; GPM serves stable so direct-download-only no longer applies) and project-plugin-architecture.md (admin2/api/flex now GPM-managed; git-sync remote-only category). Refresh the MEMORY.md pointers if the hooks change.
- Step 5: Set the plan Status to complete
Change the **Status:** line at the top of this file to ✅ Complete (YYYY-MM-DD) (today's date at execution).
- Step 6: Commit
git add CLAUDE.md docs/reference/architecture.md docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md
git commit -m "docs: record Grav 2.0.4 upgrade; GPM-managed plugins; prod runbook"
(Memory files live outside the repo; they are written directly, not committed here.)
Rollback
If any phase fails and cannot be fixed forward:
git revertthe relevant commits ongrav-2.0.4-upgrade(root repo) and theuserreposystem.yamlcommit.- Local:
make build && make start && make install-plugins. - Test server: config, content, and plugin reverts are delivered via
make content-push+make remote-fetch-content-test. The core is the exception — oncebin/gpm self-upgradehas completed it cannot downgrade, so treat a completed core upgrade as forward-only and fix forward; there is no revert for it.
⚠️ Do not run the fresh-install path (
scripts/server-install.sh) against a live server as a rollback. It doesrm -rf user; git clone, which destroys the server-only, gitignoreduser/config/plugins/git-sync.yaml(the encrypted git-sync token a clone never restores). The fresh-install path is for empty/new servers only.
Content, config, and accounts are in git, so no data restore is required — but note the core caveat above: "rollback = git" covers config/content/plugins, not a completed server core self-upgrade.
Execution outcome (2026-07-04)
Installed local versions (all at/above floors): Grav 2.0.4, admin2 2.0.10, api 1.0.7, flex-objects 1.4.4, login 3.8.11, form 9.1.10, shortcode-core 6.2.2.
Phase 1 (local): validated + shipped. Image rebuilt on 2.0.4, plugins installed via GPM, cache clears, rendering clean (home, trips, story, /admin2 login, /gpx-manager list/upload/delete all 200). Test suite: 75 passing. Test-config was re-pointed off the retired japan-korea-2026 onto the vetted italy-2026-demo data. A self-contained, gitignored testrunner account (created via make test-account with --admin-type both) makes make test runnable without the real account in .env.
Known issue — Form 9.1.10 filepond regression (blocked elsewhere, not a go/no-go blocker). The post form's filepond photo field 500s on the post-submit re-render (filepond.html.twig runs merge on a string). The journal entry still saves correctly (curl/on-disk test-post.sh passes); only the browser re-render errors, failing 6 post.spec.js UI specs. This is a stock-plugin upgrade regression, being fixed independently in the form-to-page/image-upload rework. Do not add a theme-override workaround in this upgrade — let that rework own the fix.
Tasks 3 & 4 (remote Makefile targets + server-install cleanup): shipped (committed on this branch).
Task 5 (Phase 2, remote test-env): executed and validated (2026-07-04). Sequence run: remote-git-sync-disable-test → content-push → remote-fetch-content-test → remote-upgrade-grav-test (core rc.10 → 2.0.7; stable served a newer patch than the 2.0.4 floor) → remote-update-plugins-test (all plugins to stable) → remote-content-status-test. Smoke test on https://test.intotheeast.com: / (renders), /admin (admin2 panel; note the server routes admin at /admin, not /admin2), and /gpx-manager all return 200 with no Twig/PHP errors. git-sync was re-enabled afterward at the user's request.
Config-drift gotcha (reconciled). The server's bin/gpm self-upgrade ran Grav's schema migration, which rewrote system.yaml strict_mode from the 1.7-era twig_compat: false to twig2_compat: false + twig3_compat: true. The local upgrade never triggered this because it was a fresh Docker-image build, not a self-upgrade — so the repo's system.yaml was stale and a future remote-fetch-content (reset --hard) would have reverted the server. Fix: folded the Twig 3 flags into the repo's user/config/system.yaml (committed 2e32a85, content-pushed), verified the local Grav 2.0.x container renders 200 under them, then reset the server to the new origin/main before re-enabling git-sync so its working tree was clean. versions.yaml and accounts/.htaccess drift is install-local and left to Grav to manage.
Phase 3 — Production (fresh install, NOT executed)
Production is empty, so this is a fresh install, not an upgrade — and it is documentation only. Do not run it as part of this plan.
When prod is provisioned:
- Provision creds: copy the REMOTE section of
.env.exampleinto.env.prodwith production values (never commit it). Runmake remote-env-setup-prod. - Fresh install at 2.0.4:
make remote-install-prodwithGRAV_VERSION=2.0.4in.env.prod.scripts/server-install.shinstalls core, then all ofplugins.txt—admin2/api/flex-objectsnow install purely viaphp bin/gpm install(no zip-stash; that special-casing was removed in Task 4). Thegpm.releases: stablechannel arrives with theuser/content clone. - git-sync (remote-only, manual): it is deliberately absent from
plugins.txt. Install it on the server, add the encrypted token touser/config/plugins/git-sync.yaml(server-only, gitignored — a fresh clone never restores it), and apply thefolders:array fix perdocs/working/git-sync-notes.md. Leave it disabled (make remote-git-sync-disable-prod) until the first content round-trip is validated, thenmake remote-git-sync-enable-prod. - Smoke test the prod URL as in Task 5 Step 7 (home / trip / story / admin2 login / one
/post//gpx-manager). Note the Form filepond known-issue above will surface on/postuntil the separate rework lands — the entry still saves. - Never run
scripts/server-install.shagainst a populated server (itrm -rf user; git clone, destroying the server-only git-sync token). Fresh/empty servers only.