diff --git a/.claude/.codebase-info/directory-structure.md b/.claude/.codebase-info/directory-structure.md index 20a62a8d..f6b083c7 100644 --- a/.claude/.codebase-info/directory-structure.md +++ b/.claude/.codebase-info/directory-structure.md @@ -30,5 +30,5 @@ proof9/ # Claude Code plugin: hooks/, tests/, README.md tests/ # core/, cli/, adapters/, integration/, lifecycle/, e2e/, ci/, … docs/ # GOLDEN_PATH, CLI_WIREFRAME, PRODUCT_ROADMAP, VISION, CLI guide, images/ scripts/ # lifecycle runner, quickstart-cleanroom/, ci/ helpers, verify scripts -site/ # Static codeframe.sh page (no build, no JS); make-og-image.sh rebuilds the card +site/ # Static codeframe.sh page (no build, no JS); tools/site/make-og-image.sh rebuilds the card, tools/site/deploy.sh publishes it ``` diff --git a/site/index.html b/site/index.html index ec0fa790..4c5f9f08 100644 --- a/site/index.html +++ b/site/index.html @@ -12,7 +12,7 @@ - + diff --git a/tools/site/README.md b/tools/site/README.md new file mode 100644 index 00000000..e91892b3 --- /dev/null +++ b/tools/site/README.md @@ -0,0 +1,51 @@ +# Site tooling + +Scripts for the static `codeframe.sh` page in `site/`. They live here rather than +in `site/` so that everything under `site/` is servable, and a deploy can copy the +directory as-is with no exclude list to remember. + +| Script | What it does | +|---|---| +| `make-og-image.sh` | Regenerates `site/og-image.png` (the 1200x630 social card) and `site/proof9-loop-still.png`. Needs ffmpeg and DejaVu fonts. | +| `make-banner.py` | Builds the outlined banner SVG. Usage is in its docstring. | +| `deploy.sh` | Publishes `site/` at `origin/main` to the web root. Runs on the server. | + +## Deploying + +The trigger is manual. After a change to `site/` merges: + +```bash +tailscale ssh root@frank-prod-server deploy-codeframe-site +``` + +One-time setup on the server. It needs `git`, `rsync` and the `www-data` user, which +nginx creates; the script refuses to start without that user rather than failing +part-way through. + +```bash +git clone --depth 1 https://github.com/frankbria/codeframe /opt/codeframe-site +install -m 755 /opt/codeframe-site/tools/site/deploy.sh /usr/local/bin/deploy-codeframe-site +``` + +Re-run the `install` line whenever `deploy.sh` itself changes; the installed copy +does not update itself. + +What a run does: + +- pulls the public repository, so no credential is needed and none is stored; +- keeps one rolling backup of the web root it replaces, at `/root/codeframe.sh-predeploy.tgz`. + It is skipped on a first deploy, when the root already holds the same commit, and after + an interrupted run, so a re-run after a bad or broken deploy cannot overwrite the last + good copy; +- mirrors `site/` into the web root with `rsync --delete`, so an asset removed or + renamed in the repository disappears on the next deploy; +- writes the deployed commit to `.deployed-sha` in the web root. + +Paths can be overridden with `REPO_URL`, `SRC`, `DOCROOT` and `BACKUP`, which is how +the script is tested in a throwaway container. + +Check what is live: + +```bash +tailscale ssh root@frank-prod-server cat /var/www/codeframe.sh/.deployed-sha +``` diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh new file mode 100755 index 00000000..f5531280 --- /dev/null +++ b/tools/site/deploy.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Publish site/ at origin/main to the codeframe.sh web root. +# +# Runs ON the web server, not on a workstation. Installed as +# /usr/local/bin/deploy-codeframe-site; see tools/site/README.md. +# +# The repository is public, so this pulls with no credential. rsync --delete +# makes the web root an exact copy of site/: an asset removed or renamed in the +# repository disappears here on the next run. +set -euo pipefail + +REPO_URL=${REPO_URL:-https://github.com/frankbria/codeframe} +SRC=${SRC:-/opt/codeframe-site} +DOCROOT=${DOCROOT:-/var/www/codeframe.sh} +BACKUP=${BACKUP:-/root/codeframe.sh-predeploy.tgz} +# Present only while a sync is under way. If it survives, a previous run died +# mid-sync and the web root is a torn mix of two commits. +INCOMPLETE=${INCOMPLETE:-${BACKUP%.tgz}.incomplete} + +# A clone killed part-way leaves a .git with no commit, and every later fetch +# would fail. Remove only that: a checkout with a commit is kept even if fetch +# fails, and a directory that is not a git checkout is never deleted. +if [ -d "$SRC/.git" ] && ! git -C "$SRC" rev-parse -q --verify HEAD >/dev/null 2>&1; then + rm -rf "$SRC" +fi +if [ ! -d "$SRC/.git" ]; then + git clone -q --depth 1 "$REPO_URL" "$SRC" +fi +git -C "$SRC" fetch -q --depth 1 origin main +git -C "$SRC" reset -q --hard origin/main +HEAD_SHA=$(git -C "$SRC" rev-parse HEAD) + +# Fail before touching anything if the web server user is missing, rather than +# part-way through with a live site and no record of the deploy. +id -u www-data >/dev/null 2>&1 || { echo "deploy: user www-data does not exist; create it or install nginx first" >&2; exit 1; } + +# One rolling backup of what is being replaced, taken only when the web root is +# a complete deploy of a different commit. Skipped on a first deploy (no root), +# on a re-run at the same commit, and after an interrupted run (torn root): in +# each of those cases a backup would replace the last good copy with something +# that is not one. The sync below still runs, so a re-run repairs the root. +if [ -d "$DOCROOT" ] && [ ! -e "$INCOMPLETE" ] \ + && [ "$(cat "$DOCROOT/.deployed-sha" 2>/dev/null)" != "$HEAD_SHA" ]; then + # Written aside and renamed: a tar that fails part-way (disk full) must not + # truncate the backup it was meant to replace. + tar -C "$(dirname "$DOCROOT")" -czf "$BACKUP.tmp" "$(basename "$DOCROOT")" + mv "$BACKUP.tmp" "$BACKUP" +fi + +# The marker goes down before the first change to the web root and comes up +# after the last, so no interruption can leave a changed root without it. +touch "$INCOMPLETE" +mkdir -p "$DOCROOT" +# .well-known/ is left alone in case certificate renewal ever uses this root. +rsync -a --delete --exclude '/.well-known/' --exclude '/.deployed-sha' "$SRC/site/" "$DOCROOT/" +chown -R www-data:www-data "$DOCROOT" +# Success is recorded only after the last step that can fail, so a failure +# leaves the previous sha and the marker in place for the next run to repair. +echo "$HEAD_SHA" > "$DOCROOT/.deployed-sha" +rm -f "$INCOMPLETE" + +echo "deployed ${HEAD_SHA:0:12} to $DOCROOT" diff --git a/site/make-banner.py b/tools/site/make-banner.py similarity index 98% rename from site/make-banner.py rename to tools/site/make-banner.py index 24f2d064..905d6edb 100644 --- a/site/make-banner.py +++ b/tools/site/make-banner.py @@ -9,7 +9,7 @@ Needs fontTools (and cairosvg for the PNG), plus IBM Plex Sans and IBM Plex Mono as .woff, e.g. from `npm pack @ibm/plex-sans @ibm/plex-mono` (fonts/complete/woff/). - python3 site/make-banner.py site/codeframe-lockup-reverse.svg \ + python3 tools/site/make-banner.py site/codeframe-lockup-reverse.svg \ /fonts/complete/woff /fonts/complete/woff \ codeframe_github_header.svg python3 -c "import cairosvg; cairosvg.svg2png(url='codeframe_github_header.svg', \ diff --git a/site/make-og-image.sh b/tools/site/make-og-image.sh similarity index 94% rename from site/make-og-image.sh rename to tools/site/make-og-image.sh index 40d48dae..9f1c576a 100755 --- a/site/make-og-image.sh +++ b/tools/site/make-og-image.sh @@ -3,7 +3,8 @@ # (the reduced-motion still: the demo GIF's last frame). Needs ffmpeg + DejaVu fonts. # Edit the COPY strings below when marketing copy changes, then re-run. set -euo pipefail -cd "$(dirname "$0")" +# Lives in tools/site/; its inputs and outputs are in site/. +cd "$(dirname "$0")/../../site" # COPY: card strings LINE1="Your coding agent can't quietly"