feat(deploy): repeatable Cloudflare deploy script + make targets
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Executable
+60
@@ -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/"
|
||||
Executable
+25
@@ -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
|
||||
Reference in New Issue
Block a user