Files
intotheeast-com/docs/working/plans/2026-06-18-grav2-upgrade.md
T

16 KiB

Grav 2.0 Upgrade 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.

Goal: Upgrade the local dev Docker environment from linuxserver/grav 1.7 to getgrav/grav 2.0 RC, validate the full Milestone 1 posting workflow, and update the production install script for a fresh Grav 2.0 deploy.

Architecture: Two tracks in sequence — (1) swap the Docker image and update all dependent config/paths, boot the site with make setup, run the existing test suite; (2) update server-install.sh so make remote-install deploys Grav 2.0 fresh on the production PHP 8.4 server. The user/ directory (content, config, theme, custom plugins) is already isolated as a git repo and requires only a small compatibility addition to cache-on-save.

Tech Stack: Grav CMS 2.0.0-rc.9, PHP 8.4 (production) / Docker getgrav/grav with PHP 8.3 (dev), Apache, Twig 3, Symfony 7, Playwright (UI tests).

Global Constraints

  • All work on branch update-to-2.0 (already created)
  • Never read .env — contains sensitive credentials
  • Only modify files in the project root or user/ subfolders
  • user/config/system.yaml is tracked in the user/ git repo — commit it with git -C user add config/system.yaml && git -C user commit ..., NOT from the main repo
  • user/plugins/cache-on-save/ is tracked in the main repo (after adding .gitignore exception) — commit blueprints.yaml with git add user/plugins/cache-on-save/blueprints.yaml from the project root
  • Container name stays intotheeast_grav; local port stays 8081
  • make commands are the only way to interact with the remote server
  • Grav 2.0 requires PHP ≥ 8.3 (dev container uses 8.3 default; production uses 8.4 — both compliant)
  • Production download URL format: https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX}

Files Changed

File Action Reason
docker-compose.yml Modify Switch image, update volume + PHP ini path, add env var
Makefile Modify Three docker exec targets hardcode linuxserver's /app/www/public path
.gitignore Modify Add !user/plugins/cache-on-save/ exception to track the custom plugin in the main repo
user/plugins/cache-on-save/blueprints.yaml Create Grav 2.0 compat flag (required by GPM) — committed to main repo
user/config/system.yaml Modify Switch GPM channel from stable to testing
scripts/server-install.sh Modify Support GRAV_CHANNEL_SUFFIX for ?testing query param on 2.0 RC download

Task 1: Swap Docker image and fix container paths

Files:

  • Modify: docker-compose.yml
  • Modify: Makefile

Interfaces:

  • Produces: A running Grav 2.0 container reachable at http://localhost:8081 with user/ mounted at /var/www/html/user and PHP upload limits applied via /usr/local/etc/php/conf.d/php-local.ini

  • Step 1: Stop and remove the current container

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
docker compose down

Expected: container intotheeast_grav stops and is removed.

  • Step 2: Update docker-compose.yml

Replace the entire contents of docker-compose.yml with:

services:
  grav:
    image: getgrav/grav
    container_name: intotheeast_grav
    environment:
      - GRAV_CHANNEL=beta
    ports:
      - "8081:80"
    volumes:
      - ./user:/var/www/html/user
      - ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini
    restart: unless-stopped

Key changes from old file:

  • image: lscr.io/linuxserver/grav:latestgetgrav/grav

  • environment: removed PUID/PGID (linuxserver-specific), added GRAV_CHANNEL=beta

  • volumes[0]: /config/www/user/var/www/html/user

  • volumes[1]: /config/php/php-local.ini/usr/local/etc/php/conf.d/php-local.ini

  • Step 3: Update Makefile — three targets use the old container path

In Makefile, make these three targeted replacements:

install-plugins target — change working directory flag:

Old:

install-plugins:
	docker exec -w /app/www/public intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y

New:

install-plugins:
	docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y

demo-load target — change cache clear path:

Old:

demo-load:
	cp -r user/docs/demo/tracker/. user/pages/01.tracker/
	docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"

New:

demo-load:
	cp -r user/docs/demo/tracker/. user/pages/01.tracker/
	docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"

demo-reset target — change cache clear path:

Old:

demo-reset:
	@for dir in user/docs/demo/tracker/*/; do \
		folder=$$(basename "$$dir"); \
		rm -rf "user/pages/01.tracker/$$folder"; \
	done
	docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"

New:

demo-reset:
	@for dir in user/docs/demo/tracker/*/; do \
		folder=$$(basename "$$dir"); \
		rm -rf "user/pages/01.tracker/$$folder"; \
	done
	docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
  • Step 4: Validate docker-compose syntax
docker compose config

Expected: prints merged compose config with no errors. If you see Error, re-check the YAML indentation in docker-compose.yml.

  • Step 5: Commit
git add docker-compose.yml Makefile
git commit -m "feat: switch to getgrav/grav 2.0 RC docker image

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"

Task 2: Add Grav 2.0 compat flag and switch GPM to testing channel

Files:

  • Modify: .gitignore (add exception for user/plugins/cache-on-save/)
  • Create: user/plugins/cache-on-save/blueprints.yaml (committed to main repo)
  • Modify: user/config/system.yaml (committed to user/ git repo, not main repo)

Interfaces:

  • Consumes: Running container from Task 1

  • Produces: GPM resolves 2.0-compatible plugin versions on install; cache-on-save is recognized as 2.0-compatible by Grav's plugin registry

  • Step 1: Create user/plugins/cache-on-save/blueprints.yaml

Create the file with this exact content:

name: Cache On Save
version: 1.0.0
description: Clears Grav cache on new-entry form submission
author:
  name: Mischa
  email: mischa@gorinskat.nl
license: MIT

dependencies:
  - { name: grav, version: '>=1.6.0' }

grav:
  version: ['1.7', '2.0']
  • Step 2: Update GPM channel in user/config/system.yaml

Find the gpm: section (around line 200 in the file) and change releases: stable to releases: testing:

Old:

gpm:
  releases: stable
  official_gpm_only: true

New:

gpm:
  releases: testing
  official_gpm_only: true
  • Step 3: Add gitignore exception and commit blueprints.yaml to main repo
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast

# Add exception so cache-on-save is tracked in the main repo
# Insert after the existing "user/plugins/" line in .gitignore:
# !user/plugins/cache-on-save/

# Then commit to the main repo:
git add .gitignore user/plugins/cache-on-save/blueprints.yaml
git commit -m "feat: track cache-on-save plugin in main repo; add Grav 2.0 compat flag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
  • Step 4: Commit system.yaml to the user/ git repo
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git -C user add config/system.yaml
git -C user commit -m "feat: switch GPM to testing channel for Grav 2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"

Task 3: Boot Grav 2.0 and install plugins

Files: None (runtime only)

Interfaces:

  • Consumes: docker-compose.yml from Task 1, GPM config from Task 2

  • Produces: Running Grav 2.0 instance at http://localhost:8081 with all plugins installed

  • Step 1: Run setup

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
make setup

This starts the container and installs all plugins from plugins.txt. First run may take 1-2 minutes as getgrav/grav downloads and extracts Grav 2.0 RC.

Expected output ends with something like:

GPM Packages Installed: admin, email, error, form, login, problems, add-page-by-form, shortcode-gallery-plusplus

If make setup fails on plugin install with a permission error, fix with:

docker exec intotheeast_grav chown -R www-data:www-data /var/www/html/cache /var/www/html/logs /var/www/html/tmp
make install-plugins
  • Step 2: Verify PHP upload limits are applied
docker exec intotheeast_grav php -r "echo ini_get('upload_max_filesize') . ' / ' . ini_get('post_max_size');"

Expected: 100M / 500M

If you see 2M / 8M (PHP defaults), the ini mount path is wrong. Verify with:

docker exec intotheeast_grav php -r "echo php_ini_scanned_files();"

It should include /usr/local/etc/php/conf.d/php-local.ini.

  • Step 3: Verify site loads
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/

Expected: 200

If you get 500, check container logs:

docker logs intotheeast_grav --tail 50
  • Step 4: Verify Admin2 loads
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/admin

Expected: 200 (Admin2 SPA login page, not the old Twig admin)

  • Step 5: Run config and HTTP tests
make test-config
make test-post

test-config validates the form YAML config. test-post submits the posting form via HTTP and checks an entry is created.

Expected: both exit 0.

If test-post fails, check the output of:

bash scripts/test-post.sh

This is the critical add-page-by-form go/no-go test. If it fails with a 500 or the entry isn't created, see the If add-page-by-form fails section at the bottom of this plan.

  • Step 6: Commit task completion note

No new files to commit. Move to Task 4.


Task 4: Run Playwright test suite and fix any Admin2 regressions

Files:

  • Modify: tests/*.spec.js (only if tests fail due to Admin2 DOM changes)

Interfaces:

  • Consumes: Running Grav 2.0 from Task 3

  • Produces: All Playwright tests passing (or updated for Admin2's new DOM)

  • Step 1: Run the full UI test suite

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
make test-ui

Expected: 25 tests pass.

  • Step 2: If any tests fail, classify the failure

For each failing test, determine whether it is:

A) A genuine regression (e.g., posting form broken, tracker page missing entries, gallery not rendering) — these are blockers. Stop, investigate the root cause, and fix the underlying Grav/plugin issue before updating the test.

B) An Admin2 DOM change (e.g., selectors targeting old admin HTML structure like .admin-menu, .grav-nav, admin-specific CSS classes) — these are acceptable test updates. Update the selector in the test file to match Admin2's new HTML.

To inspect the current Admin2 DOM for a failing selector:

# Check what the admin page actually renders
curl -s http://localhost:8081/admin | grep -o '<[^>]*class="[^"]*admin[^"]*"[^>]*>' | head -20
  • Step 3: Update any Admin2 selector regressions

For each type-(B) failure, open the relevant test file in tests/ and update the selector. Example pattern for updating an admin navigation selector:

Old (targeting classic admin):

await page.click('.grav-nav-toggle')

New (targeting Admin2 SPA — find actual selector from step 2's output):

await page.click('[data-testid="nav-toggle"]')  // replace with actual Admin2 selector

After each fix, re-run just that test:

npx playwright test tests/<filename>.spec.js --headed
  • Step 4: Re-run full suite to confirm all pass
make test-ui

Expected: all tests pass.

  • Step 5: Commit any test updates

If any test files were modified:

git add tests/
git commit -m "test: update Playwright selectors for Admin2 DOM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"

If no test files changed, no commit needed.


Task 5: Update production install script for Grav 2.0

Files:

  • Modify: scripts/server-install.sh

Interfaces:

  • Consumes: Nothing from prior tasks (independent of Docker)

  • Produces: make remote-install deploys a fresh Grav 2.0 on the production PHP 8.4 server when GRAV_VERSION=2.0.0-rc.9 and GRAV_CHANNEL_SUFFIX=?testing are set in .env

  • Step 1: Update the wget download line in scripts/server-install.sh

The script currently downloads Grav with:

wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip

Grav 2.0 RC requires ?testing appended to the URL. Add GRAV_CHANNEL_SUFFIX support:

Old (line ~15 in the file):

echo "==> Downloading Grav $GRAV_VERSION"
cd "$WEBROOT"
wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip

New:

echo "==> Downloading Grav $GRAV_VERSION"
cd "$WEBROOT"
wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}" -O grav-admin.zip

The ${GRAV_CHANNEL_SUFFIX:-} expands to empty string if unset, keeping stable releases working without any changes to .env.

  • Step 2: Add GRAV_CHANNEL_SUFFIX to the env var validation block

At the top of the script the required vars are validated. GRAV_CHANNEL_SUFFIX is optional, so do NOT add it to the :? required list. Instead, add a comment above the download step:

After the set -e and required var block, add a comment before the download line:

# GRAV_CHANNEL_SUFFIX: optional, set to '?testing' for RC/beta releases (e.g. 2.0.0-rc.9)
# Leave unset or empty for stable releases.
  • Step 3: Verify the script logic looks correct
# Dry-run: simulate what the URL would be with 2.0 RC vars
GRAV_VERSION=2.0.0-rc.9 GRAV_CHANNEL_SUFFIX='?testing' bash -c \
  'echo "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}"'

Expected output:

https://getgrav.org/download/core/grav-admin/2.0.0-rc.9?testing
# Dry-run: simulate stable release (no suffix)
GRAV_VERSION=1.7.53 bash -c \
  'echo "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}"'

Expected output:

https://getgrav.org/download/core/grav-admin/1.7.53
  • Step 4: Commit
git add scripts/server-install.sh
git commit -m "feat: support GRAV_CHANNEL_SUFFIX for Grav 2.0 RC production install

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"

If add-page-by-form fails (contingency)

If make test-post in Task 3 step 5 returns a non-zero exit code or the entry is not created, add-page-by-form is incompatible with Grav 2.0. The fallback is to write a custom replacement plugin.

Do not proceed to Task 4 if the posting workflow is broken. Instead:

  1. Check the container logs for the specific error:
docker logs intotheeast_grav --tail 100 | grep -i "error\|exception\|warning"
  1. Note the error, stop work, and report back. The custom replacement plugin is a separate task requiring design input from the project owner before implementation.

The custom plugin would:

  • Hook onFormProcessed (same as cache-on-save)
  • Read form field values (title, content, photo)
  • Build the page path under user/pages/01.tracker/
  • Write the page file to disk using Grav\Common\Page\Page
  • Merge cache-on-save functionality (call $this->grav['cache']->deleteAll())
  • Replace both add-page-by-form and cache-on-save with a single plugin

This is ~200 lines of PHP and ~1 day of work. It should be planned separately.


Final smoke test (after all tasks complete)

Run the full test suite one last time:

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
make test

Expected: all three suites (test-config, test-post, test-ui) exit 0.

Then verify the go/no-go criteria from the spec are all met before merging to main or deploying to production.