#!/usr/bin/env bash set -euo pipefail # Implementation for cameron-site-content-sync.service. Normal operators should # start the credential-provisioned systemd unit instead of invoking this script # from a shell that may deploy Fly and then lack the PDS credential. REPO=${CAMERON_SITE_REPO:-/home/cameron/code/cameron-site-tangled} STATE_DIR=${CAMERON_SITE_SYNC_STATE_DIR:-/home/cameron/.local/state/cameron-site} STATE_FILE="$STATE_DIR/content-source-fingerprint" LOCK_FILE="$STATE_DIR/content-sync.lock" REPORT_PATH="$STATE_DIR/last-run.json" DEPENDENCY_STATE_FILE="$STATE_DIR/dependency-source-fingerprint" if [[ -z ${CAMERON_BSKY_APP_PASSWORD:-} ]]; then echo "Refusing credential-dark content sync before Git, Fly, or PDS mutation." echo "Start cameron-site-content-sync.service so the private worker environment is loaded." exit 2 fi readonly PDS_APP_PASSWORD=$CAMERON_BSKY_APP_PASSWORD unset CAMERON_BSKY_APP_PASSWORD mkdir -p "$STATE_DIR" exec 9>"$LOCK_FILE" flock -n 9 || { echo "Another Cameron.site content sync is already running." exit 0 } cd "$REPO" retry() { local attempt delay delay=3 for attempt in 1 2 3 4 5; do if "$@"; then return 0; fi if [[ $attempt -eq 5 ]]; then return 1; fi sleep "$delay" delay=$((delay * 3)) done } changed_paths() { git status --porcelain | sed -E 's/^.. //' | sort -u } assert_receipt_only_dirty() { local unexpected unexpected=$(changed_paths | grep -Ev '^(content/about-atproto-manifest\.json|knowledge/atproto-manifest\.json)$' || true) if [[ -n "$unexpected" ]]; then echo "Refusing dirty canonical checkout; unexpected paths:" printf '%s\n' "$unexpected" exit 2 fi } commit_receipts() { if git diff --quiet -- content/about-atproto-manifest.json knowledge/atproto-manifest.json; then return 0 fi assert_receipt_only_dirty git add content/about-atproto-manifest.json knowledge/atproto-manifest.json git commit -m 'Record ATProto content projection receipts. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code ' retry git push origin main } if [[ $(git branch --show-current) != main ]]; then echo "Canonical content sync requires the main branch." exit 2 fi # Recover a prior successful PDS write whose receipt commit or push was interrupted. if [[ -n $(changed_paths) ]]; then assert_receipt_only_dirty commit_receipts fi retry git fetch origin main local_head=$(git rev-parse HEAD) remote_head=$(git rev-parse origin/main) if [[ "$local_head" != "$remote_head" ]]; then merge_base=$(git merge-base HEAD origin/main) if [[ "$merge_base" == "$remote_head" ]]; then unexpected=$( git diff --name-only origin/main..HEAD \ | grep -Ev '^(content/about-atproto-manifest\.json|knowledge/atproto-manifest\.json)$' \ || true ) if [[ -n "$unexpected" ]]; then echo "Local main is ahead with non-receipt changes; refusing automatic push:" printf '%s\n' "$unexpected" exit 2 fi retry git push origin main remote_head=$local_head elif [[ "$merge_base" != "$local_head" ]]; then echo "Canonical main diverged from origin/main; refusing automatic reconciliation." exit 2 else git merge --ff-only origin/main fi fi if [[ -n $(changed_paths) ]]; then echo "Canonical checkout became dirty during fast-forward." exit 2 fi if [[ ! -f package-lock.json ]]; then echo "The deploy checkout requires package-lock.json for reproducible dependency installation." exit 2 fi dependency_fingerprint=$( sha256sum package.json package-lock.json \ | sha256sum \ | cut -d' ' -f1 ) installed_dependency_fingerprint=$(cat "$DEPENDENCY_STATE_FILE" 2>/dev/null || true) if [[ ! -x node_modules/.bin/tsx || "$dependency_fingerprint" != "$installed_dependency_fingerprint" ]]; then npm ci --no-audit --no-fund printf '%s\n' "$dependency_fingerprint" >"$DEPENDENCY_STATE_FILE.tmp" mv "$DEPENDENCY_STATE_FILE.tmp" "$DEPENDENCY_STATE_FILE" fi pnpm typecheck pnpm test:markdown pnpm test:content pnpm test:charts pnpm test:worker if ! pnpm --silent knowledge:check >"$STATE_DIR/knowledge-check.json"; then cat "$STATE_DIR/knowledge-check.json" exit 1 fi source_fingerprint=$( git ls-files -z \ 'content/about.md' 'knowledge/published/**' 'knowledge/policy.json' \ 'src/**' 'public/**' 'package.json' 'package-lock.json' 'Dockerfile' 'fly.toml' \ ':(exclude)content/about-atproto-manifest.json' \ ':(exclude)knowledge/atproto-manifest.json' \ | sort -z \ | xargs -0 sha256sum \ | sha256sum \ | cut -d' ' -f1 ) last_fingerprint=$(cat "$STATE_FILE" 2>/dev/null || true) if [[ "$source_fingerprint" != "$last_fingerprint" ]]; then cleanup_fly_agent() { timeout 10s fly agent stop >/dev/null 2>&1 || true } trap cleanup_fly_agent EXIT fly deploy --depot=false cleanup_fly_agent trap - EXIT printf '%s\n' "$source_fingerprint" >"$STATE_FILE.tmp" mv "$STATE_FILE.tmp" "$STATE_FILE" fi pnpm --silent about:sync >"$STATE_DIR/about-plan.json" python3 - "$STATE_DIR/about-plan.json" <<'PY' import json, sys p = json.load(open(sys.argv[1])) if p["counts"]["conflict"]: raise SystemExit(f'About sync has {p["counts"]["conflict"]} conflict(s)') PY CAMERON_BSKY_APP_PASSWORD="$PDS_APP_PASSWORD" \ pnpm --silent about:sync --apply --from-origin-main >"$STATE_DIR/about-apply.json" commit_receipts pnpm --silent knowledge:sync --all >"$STATE_DIR/knowledge-plan.json" python3 - "$STATE_DIR/knowledge-plan.json" <<'PY' import json, sys p = json.load(open(sys.argv[1])) if p["counts"]["conflict"]: raise SystemExit(f'Knowledge sync has {p["counts"]["conflict"]} conflict(s)') PY CAMERON_BSKY_APP_PASSWORD="$PDS_APP_PASSWORD" \ pnpm --silent knowledge:sync --all --apply --from-origin-main >"$STATE_DIR/knowledge-apply.json" commit_receipts head=$(git rev-parse HEAD) python3 - "$STATE_DIR/about-plan.json" "$STATE_DIR/knowledge-plan.json" "$REPORT_PATH.tmp" "$head" "$source_fingerprint" <<'PY' import datetime, json, sys about = json.load(open(sys.argv[1])) knowledge = json.load(open(sys.argv[2])) report = { "completedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "head": sys.argv[4], "sourceFingerprint": sys.argv[5], "about": about["counts"], "knowledge": knowledge["counts"], "liveUrl": "https://cameron.stream/", } with open(sys.argv[3], "w") as handle: json.dump(report, handle, indent=2) handle.write("\n") PY mv "$REPORT_PATH.tmp" "$REPORT_PATH" git status --short --branch cat "$REPORT_PATH"