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.
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 REPO → USER_REPO. Add comments.
Files:
-
Modify:
.env.example -
Step 1: Replace
.env.examplecontent
# 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 ofsite-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 REPO → USER_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
-
Fill in
.env— copy.env.example, set all values. -
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.
- 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
.envis gitignored. Never commit it.GITEA_TOKENexists only in.envlocally and in~/.env-nataschaon the server during active sessions. Always runmake remote-env-removeafter use.~/.env-nataschahaschmod 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.shwrites~/.netrcfor the initial clone and deletes it immediately after.- Credentials are never passed as command-line arguments (they would appear in
psoutput). 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).