Files
natascha-rieter-nl/docs/superpowers/plans/2026-06-13-deployment-toolchain.md
T
m038 8c93dfd7c9 Sync local state with remote after untracked local evolution
Local site-ai/ had diverged significantly from the last commit pushed
to Gitea (new remote-env-setup/remove targets, SITE_CONFIG_DIR/MAIN_REPO
split, credential cleanup in server-install.sh, migration docs) without
ever being committed. This catches the repo up to what's actually on disk.

Also untracks the legacy user/ subtree left over from before content was
split into its own standalone repo (natascha-rieter.nl-user) — user/ is
gitignored here and stays a separate git repo, unaffected by this commit.
2026-08-30 14:48:54 +02:00

16 KiB
Executable File

Deployment Toolchain 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: Build a complete Make-based deployment toolchain for the natascha-rieter.nl Grav CMS site, covering initial server setup, content sync via Gitea, remote Grav management, temporary credential handling, and maintenance mode.

Architecture: Two git repos (root config repo + user content repo) are cloned on both local machine and server. The Makefile is the single interface for all operations, building SSH commands from local .env. Gitea credentials exist on the server only when explicitly written by remote-env-setup and are always removed with remote-env-remove. Content sync goes through Gitea; the Grav Sync plugin handles automatic server-side pulls. The user/ directory is a standalone nested git repo (replacing git subtree).

Tech Stack: GNU Make, Bash (scripts only where Make is insufficient), SSH, Grav CMS 1.7, Gitea, Git, Docker (local only)


File Map

Action File
Modify .env.example
Modify Makefile
Modify scripts/server-install.sh
Create scripts/server-maintenance.sh
Modify README.md

Task 1: Update .env.example

Add USER_REPO, MAIN_REPO, SITE_CONFIG_DIR. Rename REPOUSER_REPO. Add comments.

Files:

  • Modify: .env.example

  • Step 1: Replace .env.example content

# SSH connection
REMOTE_USER=root
REMOTE_HOST=example.com
REMOTE_HOME=/home/example.com

# Server paths (derived from REMOTE_HOME in Makefile; override here if needed)
WEBROOT=/home/example.com/public_html
SITE_CONFIG_DIR=/home/example.com/site-config

# Grav
GRAV_VERSION=1.7.49.5

# Repos
USER_REPO=https://gitea.example.com/org/natascha-rieter.nl-user.git
MAIN_REPO=https://gitea.example.com/org/natascha-rieter-nl.git

# Gitea credentials — never commit these; only ever in .env (local) or ~/.env-natascha (server, temporary)
GITEA_HOST=gitea.example.com
GITEA_USER=natascha-deploy
GITEA_TOKEN=your-token-here
  • Step 2: Verify .env (your local copy) has matching keys

Open .env and add any keys missing compared to .env.example. Fill in real values. Do not change .env.example values — they stay as placeholders.

  • Step 3: Commit .env.example
git add .env.example
git commit -m "config: update env.example with USER_REPO, MAIN_REPO, SITE_CONFIG_DIR"

Task 2: Migrate user/ from git subtree to standalone nested repo

The user/ directory is currently tracked by the main repo via git subtree. We convert it to a standalone git repo so content-push/content-pull are simple git push/git pull operations.

Files:

  • Modify: .gitignore (root of site-ai/)

  • No code files changed

  • Step 1: Push current user/ state via old method (safety sync)

cd /path/to/site-ai
git subtree push --prefix=user user-deploy main

Expected: pushes current user/ state to Gitea. If this fails because nothing changed, that's fine.

  • Step 2: Remove user/ from main repo tracking
git rm -r --cached user/

Expected: output like rm 'user/config/site.yaml' for each file. The files stay on disk.

  • Step 3: Add user/ to .gitignore

Create or edit .gitignore in the site-ai/ root and add:

/user/
  • Step 4: Remove the old user-deploy git remote
git remote remove user-deploy

Expected: no output, no error.

  • Step 5: Commit the removal
git add .gitignore
git commit -m "chore: remove user/ from main repo tracking (now standalone git repo)"
  • Step 6: Clone user repo fresh into user/

Replace $USER_REPO with the value from your .env.

rm -rf user/
git clone $USER_REPO user/

Expected: user/ is now a proper git repo with its own .git/ directory, tracking Gitea.

  • Step 7: Verify
git -C user status
git -C user remote -v

Expected: clean working tree, remote origin pointing to Gitea user repo.


Task 3: Update Makefile — content sync and variable defaults

Replace subtree-based targets with direct git push/pull. Allow .env to override WEBROOT and SITE_CONFIG_DIR.

Files:

  • Modify: Makefile

  • Step 1: Replace the full Makefile

-include .env
export

SSH := $(REMOTE_USER)@$(REMOTE_HOST)
WEBROOT ?= $(REMOTE_HOME)/public_html
SITE_CONFIG_DIR ?= $(REMOTE_HOME)/site-config

# ── Local dev ──────────────────────────────────────────────────────────────────

start:
	docker compose up -d

stop:
	docker compose down

setup: start install-plugins

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

# ── 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:
	@ssh $(SSH) "printf 'GITEA_HOST=%s\nGITEA_USER=%s\nGITEA_TOKEN=%s\n' \
		'$(GITEA_HOST)' '$(GITEA_USER)' '$(GITEA_TOKEN)' > ~/.env-natascha && chmod 600 ~/.env-natascha"
	@echo "Credentials written to server. Run 'make remote-env-remove' when done."

remote-env-remove:
	@ssh $(SSH) "rm -f ~/.env-natascha"
	@echo "Credentials removed from server."

# ── Remote: initial install ────────────────────────────────────────────────────

remote-install:
	ssh $(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:
	ssh $(SSH) "git -C $(SITE_CONFIG_DIR) pull"

remote-install-plugins:
	ssh $(SSH) "cd $(WEBROOT) && php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y"

remote-upgrade-grav:
	ssh $(SSH) "cd $(WEBROOT) && php bin/grav upgrade"

remote-clean:
	ssh $(SSH) "cd $(WEBROOT) && php bin/grav clearcache"

remote-maintenance-on:
	ssh $(SSH) "bash -s on $(WEBROOT)" < scripts/server-maintenance.sh

remote-maintenance-off:
	ssh $(SSH) "bash -s off $(WEBROOT)" < scripts/server-maintenance.sh

Note: indentation in Makefiles must be tabs, not spaces.

  • Step 2: Verify Make parses without errors
make -n start

Expected: prints docker compose up -d, no errors.

  • Step 3: Commit
git add Makefile
git commit -m "feat: overhaul Makefile with full deployment toolchain"

Task 4: Update scripts/server-install.sh

Add SITE_CONFIG_DIR and MAIN_REPO variables, clone the main repo to SITE_CONFIG_DIR, rename REPOUSER_REPO, print SSH public key at the end for Gitea deploy key setup.

Files:

  • Modify: scripts/server-install.sh

  • Step 1: Replace the full script

#!/bin/bash
set -e

: "${WEBROOT:?WEBROOT is not set}"
: "${SITE_CONFIG_DIR:?SITE_CONFIG_DIR is not set}"
: "${USER_REPO:?USER_REPO is not set}"
: "${MAIN_REPO:?MAIN_REPO is not set}"
: "${GRAV_VERSION:?GRAV_VERSION is not set}"
: "${PLUGINS:?PLUGINS is not set}"
: "${GITEA_HOST:?GITEA_HOST is not set}"
: "${GITEA_USER:?GITEA_USER is not set}"
: "${GITEA_TOKEN:?GITEA_TOKEN is not set}"

echo "==> Setting up credentials (temporary)"
printf 'machine %s\nlogin %s\npassword %s\n' "$GITEA_HOST" "$GITEA_USER" "$GITEA_TOKEN" > ~/.netrc
chmod 600 ~/.netrc

echo "==> Downloading Grav $GRAV_VERSION"
cd "$WEBROOT"
wget -q "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip
unzip -q grav-admin.zip
mv grav-admin/* grav-admin/.htaccess .
rm -rf grav-admin grav-admin.zip

echo "==> Cloning user repo"
rm -rf user
git clone "$USER_REPO" user

echo "==> Cloning main config repo to $SITE_CONFIG_DIR"
mkdir -p "$SITE_CONFIG_DIR"
git clone "$MAIN_REPO" "$SITE_CONFIG_DIR"

echo "==> Installing plugins"
php bin/gpm install $PLUGINS -y

echo "==> Setting permissions"
find "$WEBROOT" -type f -exec chmod 664 {} \;
find "$WEBROOT" -type d -exec chmod 775 {} \;

echo "==> Removing temporary credentials"
rm -f ~/.netrc

echo ""
echo "==> Done."
echo ""
echo "NEXT STEP — add this server's SSH public key to both Gitea repos as a deploy key"
echo "so that 'make remote-fetch' and future git pulls work without credentials:"
echo ""
cat ~/.ssh/id_rsa.pub 2>/dev/null || cat ~/.ssh/id_ed25519.pub 2>/dev/null || \
    echo "  No SSH key found. Generate one on the server: ssh-keygen -t ed25519 -C 'server-deploy'"
echo ""
echo "Visit your domain to complete Grav setup."
  • Step 2: Commit
git add scripts/server-install.sh
git commit -m "feat: server-install clones both repos, removes credentials after use"

Task 5: Create scripts/server-maintenance.sh

Toggle Grav's built-in maintenance mode by setting pages.offline in user/config/system.yaml. Grav serves its built-in offline page when this is true.

Files:

  • Create: scripts/server-maintenance.sh

  • Step 1: Create the script

#!/bin/bash
set -e

MODE="$1"
WEBROOT="$2"
CONFIG="$WEBROOT/user/config/system.yaml"

if [ "$MODE" != "on" ] && [ "$MODE" != "off" ]; then
    echo "Usage: server-maintenance.sh on|off <webroot>"
    exit 1
fi

[ -f "$CONFIG" ] || { echo "Not found: $CONFIG"; exit 1; }

VALUE="false"
[ "$MODE" = "on" ] && VALUE="true"

if grep -q "^\s*offline:" "$CONFIG"; then
    sed -i "s/^\(\s*\)offline: .*/\1offline: $VALUE/" "$CONFIG"
else
    printf '\npages:\n  offline: %s\n' "$VALUE" >> "$CONFIG"
fi

echo "Maintenance mode: $MODE (offline: $VALUE)"
  • Step 2: Make executable
chmod +x scripts/server-maintenance.sh
  • Step 3: Verify the script parses cleanly
bash -n scripts/server-maintenance.sh

Expected: no output, exit 0.

  • Step 4: Commit
git add scripts/server-maintenance.sh
git commit -m "feat: add server-maintenance.sh to toggle Grav offline mode"

Task 6: Update README.md

Full rewrite. Covers every Make command (one-line description each), setup guide, content sync workflow, and security notes.

Files:

  • Modify: README.md

  • Step 1: Replace README.md

# natascha-rieter.nl — Grav CMS

Grav CMS site for natascha-rieter.nl. Local dev via Docker; production on a VPS managed entirely through `make`.

---

## Repository structure

Two git repos:

| Repo | Contents | Location |
|------|----------|----------|
| `natascha-rieter-nl` (this repo) | Docker setup, Makefile, scripts, plugins.txt | `site-ai/` |
| `natascha-rieter.nl-user` | Site config, pages, theme | `user/` (nested git repo) |

The `user/` directory is a standalone git repo — its changes are pushed/pulled independently to Gitea. The Grav Sync plugin on the server automatically pulls from Gitea when content is pushed.

---

## Prerequisites

- Docker (for local dev)
- SSH access to the server
- Both Gitea repos created
- A Gitea personal access token with repo read/write access

---

## Local development setup

```bash
cp .env.example .env   # fill in your values
make setup             # start Docker, install plugins

Site runs at http://localhost:8080.

Clone the user content repo into user/ if not already present:

git clone $USER_REPO user/

First-time server setup

  1. Fill in .env — copy .env.example, set all values.

  2. Run the install:

make remote-install

This SSHes into the server, downloads Grav, clones both repos, installs plugins, and prints the server's SSH public key.

  1. Add the SSH key to Gitea — copy the printed public key and add it as a deploy key to both Gitea repos (read access is enough for remote-fetch; the user repo also needs write if Git Sync pushes back).

After this, make remote-fetch works without credentials.


Content sync workflow

Editors push content via the Grav Admin panel (or directly edit files in user/). The Grav Sync plugin on the server syncs automatically to Gitea.

To pull those changes locally:

make content-pull   # pull latest user content from Gitea → local user/

To push local changes back to Gitea (and trigger server sync):

git -C user add -A && git -C user commit -m "content: ..."
make content-push   # push local user/ changes → Gitea

All commands

Local

Command Description
make start Start the local Docker container
make stop Stop the local Docker container
make setup Start container and install all plugins
make install-plugins (Re)install plugins from plugins.txt in the local container
make content-push Push local user/ commits to Gitea
make content-pull Pull latest user/ content from Gitea

Remote credentials

Command Description
make remote-env-setup Write Gitea credentials to ~/.env-natascha on the server
make remote-env-remove Delete ~/.env-natascha from the server

Always run make remote-env-remove when done with operations that required it.

Remote server management

Command Description
make remote-install First-time install: download Grav, clone both repos, install plugins
make remote-fetch Pull latest main repo (Makefile, scripts, plugins.txt) on the server
make remote-install-plugins Install/update plugins from local plugins.txt on the server
make remote-upgrade-grav Upgrade Grav core on the server
make remote-clean Clear Grav cache on the server
make remote-maintenance-on Enable Grav maintenance mode (site shows offline page)
make remote-maintenance-off Disable Grav maintenance mode

Typical upgrade workflow

make remote-maintenance-on
make remote-env-setup
make remote-fetch
make remote-upgrade-grav
make remote-install-plugins
make remote-env-remove
make remote-maintenance-off
make remote-clean

Plugins

Plugins are not committed to git. The full list is in plugins.txt — one plugin name per line.

  • Locally: make install-plugins
  • On server: make remote-install-plugins

Security

  • .env is gitignored. Never commit it.
  • GITEA_TOKEN exists only in .env locally and in ~/.env-natascha on the server during active sessions. Always run make remote-env-remove after use.
  • ~/.env-natascha has chmod 600 — readable only by the SSH user.
  • The server pulls from Gitea using its SSH key (deploy key, read-only). No long-lived token is stored on the server.
  • scripts/server-install.sh writes ~/.netrc for the initial clone and deletes it immediately after.
  • Credentials are never passed as command-line arguments (they would appear in ps output). They are passed as environment variables in the SSH session.

- [ ] **Step 2: Commit**

```bash
git add README.md
git commit -m "docs: rewrite README with full command reference, setup guide, security notes"

Self-Review

Spec coverage:

  • Update initial install to set up both repos
  • content-push/pull interact with Gitea via git (Grav Sync handles server side)
  • remote-fetch (pull main repo changes on server)
  • remote-upgrade-grav
  • remote-install-plugins
  • remote-maintenance-on / remote-maintenance-off
  • remote-env-setup / remote-env-remove
  • No persistent env files on server
  • Make-first, bash only in scripts
  • SSH built from env vars
  • README updated with every command

Placeholder scan: None found.

Type consistency: N/A (shell/make, no typed interfaces).