Files
intotheeast-com/docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md
T
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

574 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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 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.yaml``gpm.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:
```dockerfile
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):
```yaml
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:
```yaml
- GRAV_CHANNEL=production
```
- [ ] **Step 5: Commit the user-repo change**
```bash
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**
```bash
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.
```bash
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**
```bash
make build
```
Expected: build completes; the RUN layer downloads `grav-admin-v2.0.4.zip`.
- [ ] **Step 3: Recreate the container**
```bash
make start
```
Expected: `intotheeast_grav` is up on http://localhost:8081.
- [ ] **Step 4: Confirm the core version is 2.0.4**
```bash
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.
```bash
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**
```bash
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**
```bash
docker exec intotheeast_grav php bin/grav cache
```
Expected: "Cache cleared" output.
- [ ] **Step 8: Assert plugin versions meet the floors**
```bash
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.
```bash
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` (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.
```bash
#!/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**
```bash
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):
```make
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`:
```make
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**
```make
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:
```make
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:
```make
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)**
```bash
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**
```bash
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`):
```bash
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):
```bash
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
bash -n scripts/server-install.sh
```
Expected: no output (valid syntax).
- [ ] **Step 4: Commit**
```bash
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.
```bash
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**
```bash
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.
```bash
make remote-fetch-content-test
```
Expected: server `user/` fast-forwards; `user/config/system.yaml` shows `releases: stable`.
- [ ] **Step 4: Upgrade the core on test**
```bash
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**
```bash
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):
```bash
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:
```bash
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**
```bash
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-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) — 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.txt``admin2`/`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.