From a68d0bc4df5a0ce0c1de4414eb3bfe5495730597 Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Tue, 30 Jun 2026 08:56:30 -0700 Subject: [PATCH] feat(deploy): repeatable Cloudflare deploy script + make targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy/cloudflare/deploy.sh (build → optional parallel R2 media sync → Pages deploy); 'make deploy' (frontend-only) / 'make deploy-media'. One-time setup stays documented in README. Guards against unauth + mis-keyed R2 uploads. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 11 +++++- deploy/cloudflare/README.md | 16 +++++++++ deploy/cloudflare/deploy.sh | 60 +++++++++++++++++++++++++++++++++ deploy/cloudflare/r2-put-one.sh | 25 ++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100755 deploy/cloudflare/deploy.sh create mode 100755 deploy/cloudflare/r2-put-one.sh diff --git a/Makefile b/Makefile index d663205..ade5356 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: sim sim-local +.PHONY: sim sim-local deploy deploy-media # Prefer the project venv's interpreter (this box has no bare `python` on PATH), # fall back to python3, then python. @@ -9,3 +9,12 @@ sim: sim-local: $(PYTHON) -m uvicorn simulator.app:app --reload --port 8000 + +# Repeatable deploy to benstull.art (Cloudflare Pages + R2). One-time setup +# (wrangler login, bucket/CORS/domain) is in deploy/cloudflare/README.md. +# `deploy` is frontend-only (fast); `deploy-media` also re-syncs media to R2. +deploy: + ./deploy/cloudflare/deploy.sh + +deploy-media: + ./deploy/cloudflare/deploy.sh --media diff --git a/deploy/cloudflare/README.md b/deploy/cloudflare/README.md index b39d979..e345995 100644 --- a/deploy/cloudflare/README.md +++ b/deploy/cloudflare/README.md @@ -10,6 +10,22 @@ Plan: `docs/superpowers/plans/2026-06-30-cloudflare-static-publish.md`. This project is **exempt from the flotilla/PPE/§9 pipeline** (operator-set) — it deploys directly to Cloudflare. Safety/hygiene rules still hold. +## Repeatable deploy (after the one-time setup below) + +Once the bucket/CORS/custom-domains + `wrangler login` exist (one-time, §1–3), every +later deploy is one command (`deploy/cloudflare/deploy.sh`, or the Make targets): + +```bash +make deploy # frontend only: build + Pages deploy (fast — most deploys) +make deploy-media # also re-sync all media to R2 (~2 min, parallel) — when media changed +./deploy/cloudflare/deploy.sh --media-only # only the R2 media sync, skip Pages +``` + +The script rebuilds `dist/`, (optionally) parallel-uploads `dist-media/` to R2, and +deploys Pages — printing the live URL. It checks `wrangler` auth first and fails +loudly. Override defaults via env: `BUCKET`, `PROJECT`, `MEDIA_BASE`, `APP_PATH`, +`BRANCH`, `JOBS`. The sections below document the one-time setup the script assumes. + ## Prerequisites (operator gestures — need Cloudflare auth) `wrangler` is not installed in the dev box; the deploy runs from a machine with diff --git a/deploy/cloudflare/deploy.sh b/deploy/cloudflare/deploy.sh new file mode 100755 index 0000000..796f531 --- /dev/null +++ b/deploy/cloudflare/deploy.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Repeatable deploy of the HEF simulator to benstull.art (Cloudflare Pages + R2). +# +# deploy/cloudflare/deploy.sh # frontend only: build + Pages deploy (fast) +# deploy/cloudflare/deploy.sh --media # also re-sync all media to R2 (~2 min, parallel) +# deploy/cloudflare/deploy.sh --media-only # only the R2 media sync, no Pages deploy +# +# Assumes the ONE-TIME setup is already done (see README.md): wrangler logged in, +# the R2 bucket + CORS + static.benstull.art custom domain, and the Pages project + +# benstull.art custom domain. This script only does the repeatable part. +# +# Override any of these via env: BUCKET, PROJECT, MEDIA_BASE, APP_PATH, BRANCH, JOBS. +set -euo pipefail + +BUCKET="${BUCKET:-human-experience-simulator-media}" +PROJECT="${PROJECT:-human-experience-simulator}" +MEDIA_BASE="${MEDIA_BASE:-https://static.benstull.art/}" +APP_PATH="${APP_PATH:-/human-experience-simulator}" +BRANCH="${BRANCH:-main}" +JOBS="${JOBS:-10}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +OUT="$REPO_ROOT/dist" +export MEDIA_OUT="$REPO_ROOT/dist-media" +PY="${PY:-$REPO_ROOT/.venv/bin/python}" +export WRANGLER="${WRANGLER:-$(command -v wrangler || echo "$HOME/.npm-global/bin/wrangler")}" + +do_media=0 +do_pages=1 +case "${1:-}" in + --media) do_media=1 ;; + --media-only) do_media=1; do_pages=0 ;; + "") ;; + *) echo "unknown flag: $1 (use --media or --media-only)"; exit 2 ;; +esac + +[ -x "$WRANGLER" ] || { command -v "$WRANGLER" >/dev/null || { echo "wrangler not found ($WRANGLER) — npm i -g wrangler"; exit 1; }; } +"$WRANGLER" whoami >/dev/null 2>&1 || { echo "not authenticated — run: $WRANGLER login"; exit 1; } + +echo "▶ build ($MEDIA_BASE)" +cd "$REPO_ROOT" +"$PY" -m tools.build_static --out "$OUT" --media-out "$MEDIA_OUT" \ + --media-base "$MEDIA_BASE" --app-path "$APP_PATH" | tail -1 + +if [ "$do_media" = 1 ]; then + echo "▶ R2 media sync → $BUCKET (parallel x$JOBS, no bulk-sync exists)" + export BUCKET + fails=$(find "$MEDIA_OUT" -type f -print0 \ + | xargs -0 -P "$JOBS" -I {} bash "$REPO_ROOT/deploy/cloudflare/r2-put-one.sh" {} \ + | tee /dev/stderr | grep -c '^FAIL ' || true) + [ "$fails" = 0 ] || { echo "✘ $fails media uploads failed"; exit 1; } + echo "✔ media synced" +fi + +if [ "$do_pages" = 1 ]; then + echo "▶ Pages deploy → $PROJECT" + "$WRANGLER" pages deploy "$OUT" --project-name "$PROJECT" --branch "$BRANCH" --commit-dirty=true | tail -3 +fi + +echo "✔ done — https://benstull.art$APP_PATH/" diff --git a/deploy/cloudflare/r2-put-one.sh b/deploy/cloudflare/r2-put-one.sh new file mode 100755 index 0000000..ae1068e --- /dev/null +++ b/deploy/cloudflare/r2-put-one.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Upload ONE file to R2 with the right content-type + immutable cache. Used by +# deploy.sh's parallel xargs (wrangler has no bulk sync). Reads BUCKET / MEDIA_OUT / +# WRANGLER from the environment; the single argument is the local file path. +set -u +f="$1" +key="${f#"$MEDIA_OUT"/}" +# Guard: if the prefix strip didn't fire (e.g. relative path vs absolute MEDIA_OUT), +# the key would wrongly keep a leading dir → wrong R2 path. Fail loudly, don't ship it. +if [ "$key" = "$f" ]; then + echo "FAIL $f (key strip failed — MEDIA_OUT='$MEDIA_OUT' is not a prefix; pass absolute paths)" + exit 1 +fi +case "${f##*.}" in + mp4) ct=video/mp4 ;; + mp3) ct=audio/mpeg ;; + json) ct=application/json ;; + *) ct=application/octet-stream ;; +esac +if "$WRANGLER" r2 object put "$BUCKET/$key" --file "$f" --remote \ + --content-type "$ct" --cache-control "public, max-age=31536000, immutable" >/dev/null 2>&1; then + echo "ok $key" +else + echo "FAIL $key" +fi