Files
intotheeast-com/docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md
m038andClaude Opus 4.8 e0e2e1e7b5 docs: mark Grav 2.0.4 plan Phase 3 (prod) executed + outcome
Prod was actually deployed (LIVE at intotheeast.com, Grav 2.0.7); the
plan previously said Phase 3 was documentation-only/not-executed. Record
the three deploy gotchas (docs/solutions/) and remaining follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-05 00:16:33 +02:00

26 KiB
Raw Permalink Blame History

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 — Phase 1 (local), Phase 2 (test env), and Phase 3 (production) all executed and shipped. Phase 3 was run for real on 2026-07-05 (see the Phase 3 section for the execution outcome and the three docs/solutions/ gotchas it produced). git-sync re-enabled on test and set up on prod. See "Known issue" below re: Form 9.1.10 filepond.

Goal: Upgrade Grav core 2.0.0-rc.102.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 only pages/ config/ accounts/ themes/ are tracked (plugins/ is gitignored except cache-on-save/ and story-blocks/). Changes to user/config/system.yaml commit to the user repo and reach the server via make content-push → server pull; everything else commits to the root repo.
  • GPM channel authority is user/config/system.yamlgpm.releases (must be stable on the server before any GPM op). GRAV_CHANNEL in docker-compose is cosmetic/consistency only.
  • Never read .env*. Use make remote-* targets for all server ops.
  • Do not touch twig.cache (stays false in dev per CLAUDE.md).
  • git-sync is remote-only: never add it to plugins.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 upgrade does 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.txt containing api, admin2, flex-objects; stable GPM 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 testingstable. 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:

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 (fix remote-upgrade-grav; add remote-update-plugins, remote-git-sync-disable, remote-git-sync-enable; register the new targets in REMOTE_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-grav target

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.txt now 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 2526 (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 4345 (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 14 (pushed to Gitea), the -test make 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-manager lists, 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:

  1. git revert the relevant commits on grav-2.0.4-upgrade (root repo) and the user repo system.yaml commit.
  2. Local: make build && make start && make install-plugins.
  3. Test server: config, content, and plugin reverts are delivered via make content-push + make remote-fetch-content-test. The core is the exception — once bin/gpm self-upgrade has 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 does rm -rf user; git clone, which destroys the server-only, gitignored user/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-testcontent-pushremote-fetch-content-testremote-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) — EXECUTED 2026-07-05

Execution outcome (2026-07-05): the fresh prod install was run for real (make remote-install-prod) and the site is live at https://intotheeast.com. The runbook below was followed, but three non-obvious gotchas surfaced — each now has its own learning in docs/solutions/:

  • Stale .env.prod GRAV_VERSION installed Grav rc.10, so GPM wouldn't serve the api plugin (needs ≥2.0.4) → Admin2 login 404'd silently. Fixed via make remote-upgrade-grav-prod (→ 2.0.7) + reinstall. See docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md. TODO: bump .env.prod GRAV_VERSION to 2.0.4 so a future fresh install doesn't repeat the RC.
  • Double Content-Encoding header (non-FastCGI host + mod_deflate) rendered a garbage page once prod switched to twig.debug: false. Fixed via debugger.shutdown.close_connection: false in the prod env override. See docs/solutions/integration-issues/grav-double-content-encoding-garbage-page.md.
  • Plugin config stranded in the untracked user/plugins/ doesn't deploy. See docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md.

Twig prod-mode is applied as a per-environment override (deploy/env/prod/system.yaml via make remote-apply-env-prod); git-sync is installed, configured, and enabled (see docs/working/git-sync-notes.md). Remaining minor follow-ups: gitignore config/security-private.php (committed salt); optional popularity.salt strip.

The original runbook (production was empty, so this was a fresh install, not an upgrade):

  1. Provision creds: copy the REMOTE section of .env.example into .env.prod with production values (never commit it). Run make remote-env-setup-prod.
  2. Fresh install at 2.0.4: make remote-install-prod with GRAV_VERSION=2.0.4 in .env.prod. scripts/server-install.sh installs core, then all of plugins.txtadmin2/api/flex-objects now install purely via php bin/gpm install (no zip-stash; that special-casing was removed in Task 4). The gpm.releases: stable channel arrives with the user/ content clone.
  3. git-sync (remote-only, manual): it is deliberately absent from plugins.txt. Install it on the server, add the encrypted token to user/config/plugins/git-sync.yaml (server-only, gitignored — a fresh clone never restores it), and apply the folders: array fix per docs/working/git-sync-notes.md. Leave it disabled (make remote-git-sync-disable-prod) until the first content round-trip is validated, then make remote-git-sync-enable-prod.
  4. 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 /post until the separate rework lands — the entry still saves.
  5. Never run scripts/server-install.sh against a populated server (it rm -rf user; git clone, destroying the server-only git-sync token). Fresh/empty servers only.