From 13aaa644db91fdf5d73f3bd5b7e10236cb79744b Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:16:13 -0700 Subject: [PATCH 1/7] build(site): move generators out of the web root, add a pull-based deploy site/ held two build scripts, make-og-image.sh and make-banner.py, alongside the files it serves. Any deploy that copied the directory had to remember to exclude them, and the one written for #1523 forgot the second. Move both to tools/site/. make-og-image.sh now cds into ../../site, so its inputs and outputs are unchanged; make-banner.py takes every path as an argument and only its usage example needed the new location. Add tools/site/deploy.sh. It runs on the web server, pulls the public repository (no credential), keeps one rolling backup, mirrors site/ into the web root with rsync --delete, and records the deployed commit in .deployed-sha. The trigger is manual, documented in tools/site/README.md. Closes #1523. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- .claude/.codebase-info/directory-structure.md | 2 +- site/index.html | 2 +- tools/site/README.md | 43 +++++++++++++++++++ tools/site/deploy.sh | 30 +++++++++++++ {site => tools/site}/make-banner.py | 2 +- {site => tools/site}/make-og-image.sh | 3 +- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tools/site/README.md create mode 100755 tools/site/deploy.sh rename {site => tools/site}/make-banner.py (98%) rename {site => tools/site}/make-og-image.sh (94%) 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 000cafa4..ba68d6d3 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..f6c7cf56 --- /dev/null +++ b/tools/site/README.md @@ -0,0 +1,43 @@ +# 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: + +```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 current web root at `/root/codeframe.sh-predeploy.tgz`; +- 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. + +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..d04759fc --- /dev/null +++ b/tools/site/deploy.sh @@ -0,0 +1,30 @@ +#!/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=https://github.com/frankbria/codeframe +SRC=/opt/codeframe-site +DOCROOT=/var/www/codeframe.sh + +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 + +# One rolling backup of whatever is being replaced. +tar -C "$(dirname "$DOCROOT")" -czf /root/codeframe.sh-predeploy.tgz "$(basename "$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/" +git -C "$SRC" rev-parse HEAD > "$DOCROOT/.deployed-sha" +chown -R www-data:www-data "$DOCROOT" + +echo "deployed $(cut -c1-12 "$DOCROOT/.deployed-sha") 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" From 191baed1d2bc859ba1da9a5e31fee907f43fcdc8 Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:43:36 -0700 Subject: [PATCH 2/7] fix(site): deploy survives a first run and a re-run after a bad deploy Two defects found in review on #1538, both reproduced and fixed: - First deploy on a fresh server aborted: the backup tar ran before anything created the web root, and `set -e` stopped the run with an error that did not name the cause. Reproduced in a clean debian container (`tar: /var/www: Cannot open`). The backup is now taken only when the web root exists, and the root is created before the sync. - A re-run at the same commit replaced the last-good backup with the tree it was about to re-sync, which right after a bad deploy is the broken one. The backup is now skipped when the web root already holds this commit. The sync still runs, so a re-run continues to repair a hand-edited web root. Paths are overridable by environment for testing. Exercised in a clean container: first deploy with /var/www absent, no-op re-run, drift repair, new-commit backup, .well-known preserved. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- tools/site/README.md | 7 ++++++- tools/site/deploy.sh | 22 +++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tools/site/README.md b/tools/site/README.md index f6c7cf56..67258479 100644 --- a/tools/site/README.md +++ b/tools/site/README.md @@ -31,11 +31,16 @@ 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 current web root at `/root/codeframe.sh-predeploy.tgz`; +- keeps one rolling backup of the web root it replaces, at `/root/codeframe.sh-predeploy.tgz`. + It is skipped on a first deploy and when the root already holds the same commit, so a + re-run after a bad 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 diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh index d04759fc..3370c534 100755 --- a/tools/site/deploy.sh +++ b/tools/site/deploy.sh @@ -9,22 +9,30 @@ # repository disappears here on the next run. set -euo pipefail -REPO_URL=https://github.com/frankbria/codeframe -SRC=/opt/codeframe-site -DOCROOT=/var/www/codeframe.sh +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} 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) -# One rolling backup of whatever is being replaced. -tar -C "$(dirname "$DOCROOT")" -czf /root/codeframe.sh-predeploy.tgz "$(basename "$DOCROOT")" +# One rolling backup of what is being replaced. Skipped on a first deploy (no +# web root yet) and when the web root already holds this commit: right after a +# bad deploy, a re-run would otherwise overwrite the last-good backup with the +# broken tree. The sync below still runs, so a re-run repairs a hand-edited root. +if [ -d "$DOCROOT" ] && [ "$(cat "$DOCROOT/.deployed-sha" 2>/dev/null)" != "$HEAD_SHA" ]; then + tar -C "$(dirname "$DOCROOT")" -czf "$BACKUP" "$(basename "$DOCROOT")" +fi +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/" -git -C "$SRC" rev-parse HEAD > "$DOCROOT/.deployed-sha" +echo "$HEAD_SHA" > "$DOCROOT/.deployed-sha" chown -R www-data:www-data "$DOCROOT" -echo "deployed $(cut -c1-12 "$DOCROOT/.deployed-sha") to $DOCROOT" +echo "deployed ${HEAD_SHA:0:12} to $DOCROOT" From 1288f72544776da08a4e74b507494018c6e0f6d2 Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:54:53 -0700 Subject: [PATCH 3/7] fix(site): an interrupted deploy no longer costs the last-good backup Found in review on #1538. A run killed mid-rsync leaves the web root a torn mix of two commits while .deployed-sha still names the old one, so the next run saw a different commit and backed the torn tree up over the good one. A marker now exists only while a sync is under way. If it survives, the previous run did not finish and the backup is skipped. The sync still runs, so the re-run repairs the torn root and then clears the marker. The marker lives beside the backup, not in the web root, so it is never served. Exercised in a clean container alongside the earlier cases: a re-run after a simulated interruption leaves the last-good backup byte-identical, repairs the root, and clears the marker. 15/15 checks pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- tools/site/README.md | 5 +++-- tools/site/deploy.sh | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tools/site/README.md b/tools/site/README.md index 67258479..094f49dc 100644 --- a/tools/site/README.md +++ b/tools/site/README.md @@ -32,8 +32,9 @@ 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 and when the root already holds the same commit, so a - re-run after a bad deploy cannot overwrite the last good copy; + 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. diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh index 3370c534..89d51c74 100755 --- a/tools/site/deploy.sh +++ b/tools/site/deploy.sh @@ -13,6 +13,9 @@ 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} if [ ! -d "$SRC/.git" ]; then git clone -q --depth 1 "$REPO_URL" "$SRC" @@ -21,18 +24,22 @@ 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) -# One rolling backup of what is being replaced. Skipped on a first deploy (no -# web root yet) and when the web root already holds this commit: right after a -# bad deploy, a re-run would otherwise overwrite the last-good backup with the -# broken tree. The sync below still runs, so a re-run repairs a hand-edited root. -if [ -d "$DOCROOT" ] && [ "$(cat "$DOCROOT/.deployed-sha" 2>/dev/null)" != "$HEAD_SHA" ]; then +# 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 tar -C "$(dirname "$DOCROOT")" -czf "$BACKUP" "$(basename "$DOCROOT")" fi mkdir -p "$DOCROOT" +touch "$INCOMPLETE" # .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/" echo "$HEAD_SHA" > "$DOCROOT/.deployed-sha" chown -R www-data:www-data "$DOCROOT" +rm -f "$INCOMPLETE" echo "deployed ${HEAD_SHA:0:12} to $DOCROOT" From 72a168d2c53dabfde7d40354871f5166be22171c Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:57:16 -0700 Subject: [PATCH 4/7] fix(site): a failed backup no longer truncates the previous one Found in review on #1538. tar wrote the backup in place, so a tar that failed part-way (disk full, read error) aborted the deploy but left a truncated archive where the last-good backup had been: the safety mechanism destroyed what it protected. It now writes to a temporary file and renames on success. Tested with a tar shim that writes a truncated archive and exits 2: the deploy aborts before any sync, the previous backup is byte-identical, and the next normal run succeeds with no stale temp file. The earlier 15 deploy checks still pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- tools/site/deploy.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh index 89d51c74..9efa0580 100755 --- a/tools/site/deploy.sh +++ b/tools/site/deploy.sh @@ -31,7 +31,10 @@ HEAD_SHA=$(git -C "$SRC" rev-parse HEAD) # 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 - tar -C "$(dirname "$DOCROOT")" -czf "$BACKUP" "$(basename "$DOCROOT")" + # 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 mkdir -p "$DOCROOT" From 48ec7f83b8df3f21c856df5040d9fed3c170c3f0 Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:08:25 -0700 Subject: [PATCH 5/7] fix(site): record a deploy only after its last fallible step Found in review on #1538. .deployed-sha was written before chown, so a chown failure (say, no www-data user on a fresh server) recorded a successful deploy and stranded the in-progress marker, which then silently suppressed every later backup. Two changes. The script now refuses to start, before any backup or sync, if www-data does not exist, and says so. And success is recorded only after chown, so any later failure leaves the previous sha and the marker for the next run to repair. The README's one-time setup now names the prerequisites. Tested in a clean container: with www-data removed the script exits 1 with the reason and leaves no web root and no marker; with chown failing after the sync it records no success and keeps the marker, and the next normal run records the sha and clears it. Earlier suites still pass (15/15, 6/6). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- tools/site/README.md | 4 +++- tools/site/deploy.sh | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/site/README.md b/tools/site/README.md index 094f49dc..e91892b3 100644 --- a/tools/site/README.md +++ b/tools/site/README.md @@ -18,7 +18,9 @@ The trigger is manual. After a change to `site/` merges: tailscale ssh root@frank-prod-server deploy-codeframe-site ``` -One-time setup on the server: +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 diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh index 9efa0580..55b450b8 100755 --- a/tools/site/deploy.sh +++ b/tools/site/deploy.sh @@ -24,6 +24,10 @@ 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 @@ -41,8 +45,10 @@ mkdir -p "$DOCROOT" touch "$INCOMPLETE" # .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/" -echo "$HEAD_SHA" > "$DOCROOT/.deployed-sha" 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" From 8d473e294c4698a877d730cf351b645d7ede2acb Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:15:44 -0700 Subject: [PATCH 6/7] fix(site): set the deploy marker before the web root is created Found in review on #1538. mkdir ran before the in-progress marker, so a run killed between the two left an empty web root with no marker, which the next run took for a complete deploy and archived over the last-good backup. The marker now goes down before the first change to the web root and comes up after the last. That is the invariant the earlier fixes rely on; this was the one window it did not cover. Tested in a clean container: web root removed, marker set as a killed run would leave it, then a re-run keeps the last-good backup byte-identical, rebuilds the root and clears the marker. All earlier deploy suites still pass (15, 6, 10). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- tools/site/deploy.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh index 55b450b8..78108ec4 100755 --- a/tools/site/deploy.sh +++ b/tools/site/deploy.sh @@ -41,8 +41,10 @@ if [ -d "$DOCROOT" ] && [ ! -e "$INCOMPLETE" ] \ mv "$BACKUP.tmp" "$BACKUP" fi -mkdir -p "$DOCROOT" +# 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" From cc4e4f3c2a78b58be4fc2fed4f1b5059c8e758fe Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:28:13 -0700 Subject: [PATCH 7/7] fix(site): recover from a clone killed part-way Found in review on #1538. A first clone killed part-way (OOM, reboot, dropped session) left a .git with no commit; every later run skipped the clone and failed at fetch, with nothing naming the recovery. The script now removes the checkout and clones again when $SRC/.git has no commit. Narrower than the suggested fix on purpose: SRC is overridable, so the removal only touches a git checkout with no commit. A checkout that has a commit is kept even when fetch fails, and a directory that is not a git checkout is never deleted. Tested in a clean container: a wedged checkout is re-cloned and deployed; a healthy checkout whose fetch fails aborts the run and survives; SRC pointed at an unrelated directory refuses and leaves it intact. All earlier deploy suites still pass (15, 6, 10, 5). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cEymaSpmZ1arjngxx1VNM --- tools/site/deploy.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/site/deploy.sh b/tools/site/deploy.sh index 78108ec4..f5531280 100755 --- a/tools/site/deploy.sh +++ b/tools/site/deploy.sh @@ -17,6 +17,12 @@ BACKUP=${BACKUP:-/root/codeframe.sh-predeploy.tgz} # 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