diff --git a/README.md b/README.md index d95bada..3a70933 100644 --- a/README.md +++ b/README.md @@ -90,33 +90,46 @@ pnpm knowledge:promote example \ --review-receipt-digest sha256:private-receipt-digest \ --confirm-public -# Preview the one approved Standard.site portability canary. -pnpm knowledge:sync -- --slug public-knowledge +# Preview the reviewed Standard.site Knowledge projection without writing. +pnpm knowledge:sync -- --all -# Reconcile that exact reviewed canary. There is deliberately no bulk mode. -pnpm knowledge:sync -- --slug public-knowledge --apply +# After reviewed source reaches main, run the credential-provisioned worker. +systemctl --user start cameron-site-content-sync.service +systemctl --user show cameron-site-content-sync.service \ + --property=ActiveState,SubState,Result,ExecMainStatus --no-pager + +# Install or refresh the isolated worker checkout and user units. +deploy/systemd/install-content-sync-worker.sh ``` `knowledge/policy.json` is default-deny and owns the Obsidian source mappings. Draft Markdown is gitignored under `knowledge/staged/`. The Docker image copies only `knowledge/published/`, so a local draft cannot leak through a broad build -context. Canonical Knowledge remains reviewed local Markdown. The guarded sync -command is authorized only for the reviewed `public-knowledge` portability -canary in `knowledge/atproto-manifest.json`; it requires an exact slug and has no -bulk mode. The public file stores only a digest of the private route-scoped -review receipt. Promotion, protocol mirroring, and deployment are separate -actions. +context. Canonical Knowledge remains reviewed local Markdown. The projection +worker reconciles only approved entries recorded through the review pipeline and +stores public URI, CID, source-digest, and review-digest receipts in +`knowledge/atproto-manifest.json`. The public file stores only a digest of the +private route-scoped review receipt. Promotion, protocol mirroring, and +deployment are separate actions joined by the worker. ## Deployment -Deployed to Fly.io as `cameron-stream`: +Deployed to Fly.io as `cameron-stream`. The normal Git-backed content path is the +credential-provisioned worker above. A direct Fly deployment updates the site +image only; it does not reconcile About or Knowledge records on ATProto: ```bash fly deploy ``` The Fly runtime does not need a PDS credential. Projection writes happen only -through the guarded local worker. +through the guarded local worker. Do not invoke +`scripts/sync-git-backed-content-from-origin.sh` from an ordinary shell: that +shell normally lacks the worker credential and can deploy Fly successfully +before failing at ATProto reconciliation. The installed service operates from a +dedicated checkout under `~/.local/share/cameron-site/deploy-checkout`; human and +agent work in `/home/cameron/code/cameron-site-tangled` cannot make the deploy +checkout dirty or ahead of Tangled `main`. ## Stack diff --git a/deploy/systemd/cameron-site-content-sync.service b/deploy/systemd/cameron-site-content-sync.service index 38843ea..cce62f7 100644 --- a/deploy/systemd/cameron-site-content-sync.service +++ b/deploy/systemd/cameron-site-content-sync.service @@ -5,11 +5,11 @@ Wants=network-online.target [Service] Type=oneshot -WorkingDirectory=/home/cameron/code/cameron-site-tangled +WorkingDirectory=/home/cameron Environment=HOME=/home/cameron Environment=PATH=/home/cameron/.local/bin:/home/cameron/.nvm/versions/node/v22.5.1/bin:/usr/local/bin:/usr/bin:/bin EnvironmentFile=/home/cameron/.config/cameron-site/sync.env -ExecStart=/home/cameron/code/cameron-site-tangled/scripts/sync-git-backed-content-from-origin.sh +ExecStart=/home/cameron/.local/share/cameron-site/bin/run-content-sync-worker.sh TimeoutStartSec=20min UMask=0077 NoNewPrivileges=true diff --git a/deploy/systemd/install-content-sync-worker.sh b/deploy/systemd/install-content-sync-worker.sh new file mode 100755 index 0000000..7bfc1c1 --- /dev/null +++ b/deploy/systemd/install-content-sync-worker.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install the user-level deployment worker without running a deployment. The +# next timer fire (or an explicit service start) performs reconciliation. + +umask 077 + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +INSTALL_ROOT=${CAMERON_SITE_INSTALL_ROOT:-/home/cameron/.local/share/cameron-site} +WORKER_REPO=${CAMERON_SITE_WORKER_REPO:-$INSTALL_ROOT/deploy-checkout} +ORIGIN_URL=${CAMERON_SITE_ORIGIN_URL:-git@tangled.org:cameron.stream/3mjfhjorj6s22} +UNIT_DIR=${CAMERON_SITE_SYSTEMD_USER_DIR:-/home/cameron/.config/systemd/user} +SYSTEMCTL=${CAMERON_SITE_SYSTEMCTL:-systemctl} + +if "$SYSTEMCTL" --user is-active --quiet cameron-site-content-sync.service; then + echo "Content sync is active; wait for it to finish before reinstalling the worker." + exit 2 +fi + +mkdir -p "$INSTALL_ROOT/bin" "$UNIT_DIR" "$(dirname "$WORKER_REPO")" +install -m 0755 "$REPO_ROOT/scripts/run-content-sync-worker.sh" "$INSTALL_ROOT/bin/run-content-sync-worker.sh" + +if [[ ! -e "$WORKER_REPO/.git" ]]; then + if [[ -e "$WORKER_REPO" && -n $(find "$WORKER_REPO" -mindepth 1 -maxdepth 1 -print -quit) ]]; then + echo "Refusing non-empty non-Git deploy checkout: $WORKER_REPO" + exit 2 + fi + temporary_checkout="${WORKER_REPO}.tmp.$$" + rm -rf "$temporary_checkout" + trap 'rm -rf "$temporary_checkout"' EXIT + GIT_TERMINAL_PROMPT=0 git clone --single-branch --branch main "$ORIGIN_URL" "$temporary_checkout" + rmdir "$WORKER_REPO" 2>/dev/null || true + mv "$temporary_checkout" "$WORKER_REPO" + trap - EXIT +fi + +if [[ $(git -C "$WORKER_REPO" remote get-url origin) != "$ORIGIN_URL" ]]; then + echo "Refusing deploy checkout with unexpected origin: $WORKER_REPO" + exit 2 +fi + +if [[ $(git -C "$WORKER_REPO" branch --show-current) != main ]]; then + echo "Deploy checkout must remain on main: $WORKER_REPO" + exit 2 +fi + +"$SYSTEMCTL" --user stop cameron-site-content-sync.timer 2>/dev/null || true +service_tmp="$UNIT_DIR/.cameron-site-content-sync.service.$$" +trap 'rm -f "$service_tmp"' EXIT +sed "s#^ExecStart=.*#ExecStart=$INSTALL_ROOT/bin/run-content-sync-worker.sh#" \ + "$REPO_ROOT/deploy/systemd/cameron-site-content-sync.service" >"$service_tmp" +install -m 0644 "$service_tmp" "$UNIT_DIR/cameron-site-content-sync.service" +rm -f "$service_tmp" +trap - EXIT +install -m 0644 "$REPO_ROOT/deploy/systemd/cameron-site-content-sync.timer" "$UNIT_DIR/cameron-site-content-sync.timer" +"$SYSTEMCTL" --user daemon-reload +"$SYSTEMCTL" --user enable --now cameron-site-content-sync.timer + +echo "Installed isolated Cameron.site content worker." +echo "Deploy checkout: $WORKER_REPO" +echo "The installer did not start a deployment." diff --git a/docs/public-content.md b/docs/public-content.md index f25a9f8..8752bc3 100644 --- a/docs/public-content.md +++ b/docs/public-content.md @@ -38,14 +38,40 @@ review, projection, and receipt contract is documented in ## Automatic worker -`scripts/sync-git-backed-content-from-origin.sh` is the deployment and projection -worker for the Git-backed surfaces only: About, Knowledge, NOW, and site code. -It does not read, snapshot, compile, write, or reconcile Blog records. - -The worker takes a process lock, recovers interrupted receipt pushes, -fast-forwards a clean canonical checkout, runs the repository gates, deploys -changed source to Fly, reconciles About and Knowledge with CID guards, commits -only their receipt manifests, and writes a credential-free completion receipt -under `~/.local/state/cameron-site/`. +`cameron-site-content-sync.service` is the operational entry point for the +deployment and projection worker. Start it manually with: + +```bash +systemctl --user start cameron-site-content-sync.service +systemctl --user show cameron-site-content-sync.service \ + --property=ActiveState,SubState,Result,ExecMainStatus --no-pager +``` + +The service loads the private worker environment and runs +`scripts/sync-git-backed-content-from-origin.sh` for the Git-backed surfaces +only: About, Knowledge, NOW, and site code. The script is an implementation +detail, not the normal shell entry point. A credential-dark direct run can +deploy Fly and then fail before PDS reconciliation. Neither path reads, +snapshots, compiles, writes, or reconciles Blog records. + +The user unit enters through a stable installed runner and operates on the +dedicated clone at `~/.local/share/cameron-site/deploy-checkout`. The development +clone is never its working tree. Install or refresh the runner and units with: + +```bash +deploy/systemd/install-content-sync-worker.sh +``` + +The installer does not deploy. The worker later takes process locks, recovers +interrupted receipt pushes, fast-forwards its clean checkout, installs exact +locked dependencies when their fingerprint changes, runs the repository gates, +deploys changed source to Fly, reconciles About and Knowledge with CID guards, +commits only their receipt manifests, and writes a credential-free completion +receipt under `~/.local/state/cameron-site/`. + +The service verifies that the PDS credential exists, then removes it from the +worker environment before dependency installation, tests, Git operations, and +Fly deployment. Only the two bounded ATProto apply subprocesses receive it. +Read-only plans and failed test diagnostics remain credential-dark. The installed user timer runs every 15 minutes from `deploy/systemd/`. diff --git a/docs/public-knowledge.md b/docs/public-knowledge.md index 4812d53..aa4ec37 100644 --- a/docs/public-knowledge.md +++ b/docs/public-knowledge.md @@ -212,16 +212,31 @@ pnpm knowledge:promote example \ --review-receipt-digest sha256:... \ --confirm-public -# Deploy and reconcile reviewed source from clean origin/main. -scripts/sync-git-backed-content-from-origin.sh +# Deploy and reconcile reviewed source through the credential-provisioned unit. +systemctl --user start cameron-site-content-sync.service +systemctl --user show cameron-site-content-sync.service \ + --property=ActiveState,SubState,Result,ExecMainStatus --no-pager # Preview one projected record or the complete reviewed collection. pnpm knowledge:sync --slug public-knowledge pnpm knowledge:sync --all - -# Reconcile from a clean main checkout synchronized with origin/main. -pnpm knowledge:sync --all --apply --from-origin-main ``` Standard.site mirroring remains a different operation from Markdown promotion and review. It is automatic only after the source has entered clean Git `main`. +The shell script behind the systemd service is an implementation detail. Direct +invocation requires the complete private worker environment; a normal shell can +otherwise complete the Fly deployment and then stop at the missing PDS +credential, leaving a partial run for the service to recover. +The same boundary applies to `knowledge:sync --apply`: use the service for +writes, and keep direct CLI calls credential-free and read-only. + +The service uses `~/.local/share/cameron-site/deploy-checkout`, a dedicated clone +that no human or agent edits. This prevents local development commits, worktree +changes, or an ahead `main` in the development clone from blocking automatic +deployment. `deploy/systemd/install-content-sync-worker.sh` installs the stable +runner and user units but deliberately does not start a deployment. + +The service does not expose the PDS credential to dependency installation, +tests, Git, or Fly. It passes the credential only to the bounded About and +Knowledge apply subprocesses after the credential-dark plans and checks pass. diff --git a/package.json b/package.json index 5bed7fb..7d85282 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "test:markdown": "tsx --test src/markdown.test.ts", "test:content": "tsx --test src/about-content.test.ts src/blog-data.test.ts src/knowledge.test.ts src/knowledge-landing.test.tsx", "test:charts": "tsx --test src/charts/charts.test.ts src/knowledge-charts/knowledge-charts.test.tsx", - "test": "pnpm test:markdown && pnpm test:content && pnpm test:charts && pnpm knowledge:check", + "test:worker": "bash scripts/test-content-sync-worker.sh", + "test": "pnpm test:markdown && pnpm test:content && pnpm test:charts && pnpm test:worker && pnpm knowledge:check", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/scripts/run-content-sync-worker.sh b/scripts/run-content-sync-worker.sh new file mode 100755 index 0000000..0ce1a75 --- /dev/null +++ b/scripts/run-content-sync-worker.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Stable systemd entry point for the Cameron.site content worker. The deploy +# checkout is intentionally separate from the human/agent development clone so +# ordinary site work cannot make production reconciliation dirty or ahead. + +umask 077 + +WORKER_REPO=${CAMERON_SITE_WORKER_REPO:-/home/cameron/.local/share/cameron-site/deploy-checkout} +ORIGIN_URL=${CAMERON_SITE_ORIGIN_URL:-git@tangled.org:cameron.stream/3mjfhjorj6s22} +STATE_DIR=${CAMERON_SITE_SYNC_STATE_DIR:-/home/cameron/.local/state/cameron-site} +BOOTSTRAP_LOCK="$STATE_DIR/deploy-checkout.lock" + +mkdir -p "$STATE_DIR" "$(dirname "$WORKER_REPO")" +exec 8>"$BOOTSTRAP_LOCK" +flock -n 8 || { + echo "Another Cameron.site worker bootstrap is already running." + exit 0 +} + +if [[ -L "$WORKER_REPO" ]]; then + echo "Refusing symlinked deploy checkout: $WORKER_REPO" + exit 2 +fi + +if [[ ! -e "$WORKER_REPO/.git" ]]; then + if [[ -e "$WORKER_REPO" && -n $(find "$WORKER_REPO" -mindepth 1 -maxdepth 1 -print -quit) ]]; then + echo "Refusing non-empty non-Git deploy checkout: $WORKER_REPO" + exit 2 + fi + + temporary_checkout="${WORKER_REPO}.tmp.$$" + rm -rf "$temporary_checkout" + trap 'rm -rf "$temporary_checkout"' EXIT + GIT_TERMINAL_PROMPT=0 git clone --single-branch --branch main "$ORIGIN_URL" "$temporary_checkout" + rmdir "$WORKER_REPO" 2>/dev/null || true + mv "$temporary_checkout" "$WORKER_REPO" + trap - EXIT +fi + +if [[ $(git -C "$WORKER_REPO" rev-parse --is-inside-work-tree 2>/dev/null || true) != true ]]; then + echo "Deploy checkout is not a Git working tree: $WORKER_REPO" + exit 2 +fi + +actual_origin=$(git -C "$WORKER_REPO" remote get-url origin) +if [[ "$actual_origin" != "$ORIGIN_URL" ]]; then + echo "Deploy checkout origin mismatch." + echo "Expected: $ORIGIN_URL" + echo "Actual: $actual_origin" + exit 2 +fi + +if [[ $(git -C "$WORKER_REPO" branch --show-current) != main ]]; then + echo "Deploy checkout must remain on main: $WORKER_REPO" + exit 2 +fi + +worker="$WORKER_REPO/scripts/sync-git-backed-content-from-origin.sh" +if [[ ! -x "$worker" ]]; then + echo "Deploy checkout has no executable content worker: $worker" + exit 2 +fi + +export CAMERON_SITE_REPO="$WORKER_REPO" +export CAMERON_SITE_SYNC_STATE_DIR="$STATE_DIR" +cd "$WORKER_REPO" +exec "$worker" diff --git a/scripts/sync-git-backed-content-from-origin.sh b/scripts/sync-git-backed-content-from-origin.sh index f90c574..2ba6d49 100755 --- a/scripts/sync-git-backed-content-from-origin.sh +++ b/scripts/sync-git-backed-content-from-origin.sh @@ -1,11 +1,24 @@ #!/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" @@ -97,9 +110,28 @@ if [[ -n $(changed_paths) ]]; then 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 @@ -108,7 +140,7 @@ fi source_fingerprint=$( git ls-files -z \ 'content/about.md' 'knowledge/published/**' 'knowledge/policy.json' \ - 'src/**' 'public/**' 'package.json' 'pnpm-lock.yaml' 'Dockerfile' 'fly.toml' \ + 'src/**' 'public/**' 'package.json' 'package-lock.json' 'Dockerfile' 'fly.toml' \ ':(exclude)content/about-atproto-manifest.json' \ ':(exclude)knowledge/atproto-manifest.json' \ | sort -z \ @@ -130,7 +162,8 @@ p = json.load(open(sys.argv[1])) if p["counts"]["conflict"]: raise SystemExit(f'About sync has {p["counts"]["conflict"]} conflict(s)') PY -pnpm --silent about:sync --apply --from-origin-main >"$STATE_DIR/about-apply.json" +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" @@ -140,7 +173,8 @@ p = json.load(open(sys.argv[1])) if p["counts"]["conflict"]: raise SystemExit(f'Knowledge sync has {p["counts"]["conflict"]} conflict(s)') PY -pnpm --silent knowledge:sync --all --apply --from-origin-main >"$STATE_DIR/knowledge-apply.json" +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) diff --git a/scripts/test-content-sync-worker.sh b/scripts/test-content-sync-worker.sh new file mode 100755 index 0000000..5e1f8d3 --- /dev/null +++ b/scripts/test-content-sync-worker.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +mkdir -p "$tmp/source/scripts" "$tmp/fake-bin" +cd "$tmp/source" +git init -q -b main +git config user.name 'Content worker test' +git config user.email 'content-worker-test@example.invalid' +cat >scripts/sync-git-backed-content-from-origin.sh <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf 'pwd=%s\nrepo=%s\nstate=%s\n' "$PWD" "$CAMERON_SITE_REPO" "$CAMERON_SITE_SYNC_STATE_DIR" >"$CAMERON_SITE_SYNC_STATE_DIR/stub-receipt" +EOF +chmod 0755 scripts/sync-git-backed-content-from-origin.sh +git add scripts/sync-git-backed-content-from-origin.sh +git commit -q -m 'Add stub content worker' +git clone -q --bare . "$tmp/origin.git" + +CAMERON_SITE_WORKER_REPO="$tmp/deploy-checkout" \ +CAMERON_SITE_ORIGIN_URL="$tmp/origin.git" \ +CAMERON_SITE_SYNC_STATE_DIR="$tmp/state" \ + "$REPO_ROOT/scripts/run-content-sync-worker.sh" + +grep -Fx "pwd=$tmp/deploy-checkout" "$tmp/state/stub-receipt" >/dev/null +grep -Fx "repo=$tmp/deploy-checkout" "$tmp/state/stub-receipt" >/dev/null +grep -Fx "state=$tmp/state" "$tmp/state/stub-receipt" >/dev/null + +set +e +CAMERON_SITE_WORKER_REPO="$tmp/deploy-checkout" \ +CAMERON_SITE_ORIGIN_URL="$tmp/wrong-origin.git" \ +CAMERON_SITE_SYNC_STATE_DIR="$tmp/state" \ + "$REPO_ROOT/scripts/run-content-sync-worker.sh" >"$tmp/mismatch.log" 2>&1 +mismatch_rc=$? +set -e +[[ $mismatch_rc -eq 2 ]] +grep -F 'Deploy checkout origin mismatch.' "$tmp/mismatch.log" >/dev/null + +cat >"$tmp/fake-bin/systemctl" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >>"$CAMERON_SITE_TEST_SYSTEMCTL_LOG" +if [[ "$*" == *'is-active'* ]]; then exit 1; fi +exit 0 +EOF +chmod 0755 "$tmp/fake-bin/systemctl" + +CAMERON_SITE_INSTALL_ROOT="$tmp/install" \ +CAMERON_SITE_WORKER_REPO="$tmp/installed-deploy-checkout" \ +CAMERON_SITE_ORIGIN_URL="$tmp/origin.git" \ +CAMERON_SITE_SYSTEMD_USER_DIR="$tmp/units" \ +CAMERON_SITE_SYSTEMCTL="$tmp/fake-bin/systemctl" \ +CAMERON_SITE_TEST_SYSTEMCTL_LOG="$tmp/systemctl.log" \ + "$REPO_ROOT/deploy/systemd/install-content-sync-worker.sh" >"$tmp/installer.log" + +test -x "$tmp/install/bin/run-content-sync-worker.sh" +test -f "$tmp/installed-deploy-checkout/.git/HEAD" +test -f "$tmp/units/cameron-site-content-sync.service" +test -f "$tmp/units/cameron-site-content-sync.timer" +grep -Fx "ExecStart=$tmp/install/bin/run-content-sync-worker.sh" "$tmp/units/cameron-site-content-sync.service" >/dev/null +grep -F -- '--user daemon-reload' "$tmp/systemctl.log" >/dev/null +grep -F -- '--user enable --now cameron-site-content-sync.timer' "$tmp/systemctl.log" >/dev/null +grep -F 'The installer did not start a deployment.' "$tmp/installer.log" >/dev/null + +set +e +env -u CAMERON_BSKY_APP_PASSWORD \ + CAMERON_SITE_REPO="$REPO_ROOT" \ + CAMERON_SITE_SYNC_STATE_DIR="$tmp/credential-dark-state" \ + "$REPO_ROOT/scripts/sync-git-backed-content-from-origin.sh" >"$tmp/credential-dark.log" 2>&1 +credential_rc=$? +set -e +[[ $credential_rc -eq 2 ]] +grep -F 'Refusing credential-dark content sync before Git, Fly, or PDS mutation.' "$tmp/credential-dark.log" >/dev/null + +mkdir -p "$tmp/scope-source/scripts" "$tmp/scope-source/content" "$tmp/scope-source/knowledge" "$tmp/scope-bin" +cp "$REPO_ROOT/scripts/sync-git-backed-content-from-origin.sh" "$tmp/scope-source/scripts/" +cat >"$tmp/scope-source/package.json" <<'EOF' +{"name":"content-worker-scope-test","private":true} +EOF +cat >"$tmp/scope-source/package-lock.json" <<'EOF' +{"name":"content-worker-scope-test","lockfileVersion":3,"packages":{}} +EOF +cat >"$tmp/scope-source/content/about-atproto-manifest.json" <<'EOF' +{} +EOF +cat >"$tmp/scope-source/knowledge/atproto-manifest.json" <<'EOF' +{} +EOF +cd "$tmp/scope-source" +git init -q -b main +git config user.name 'Credential scope test' +git config user.email 'credential-scope-test@example.invalid' +git add . +git commit -q -m 'Add credential scope fixture' +git clone -q --bare . "$tmp/scope-origin.git" +git remote add origin "$tmp/scope-origin.git" +git fetch -q origin main + +cat >"$tmp/scope-bin/npm" <<'EOF' +#!/usr/bin/env bash +[[ -z ${CAMERON_BSKY_APP_PASSWORD:-} ]] || exit 91 +mkdir -p node_modules/.bin +touch node_modules/.bin/tsx +chmod 0755 node_modules/.bin/tsx +EOF +cat >"$tmp/scope-bin/fly" <<'EOF' +#!/usr/bin/env bash +[[ -z ${CAMERON_BSKY_APP_PASSWORD:-} ]] || exit 91 +EOF +cat >"$tmp/scope-bin/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +case "$*" in + '--silent about:sync --apply --from-origin-main') + [[ ${CAMERON_BSKY_APP_PASSWORD:-} == scope-test-secret ]] || exit 92 + echo '{"counts":{"conflict":0}}' + ;; + '--silent knowledge:sync --all --apply --from-origin-main') + [[ ${CAMERON_BSKY_APP_PASSWORD:-} == scope-test-secret ]] || exit 92 + echo '{"counts":{"conflict":0}}' + ;; + '--silent about:sync'|'--silent knowledge:sync --all') + [[ -z ${CAMERON_BSKY_APP_PASSWORD:-} ]] || exit 91 + echo '{"counts":{"conflict":0}}' + ;; + *) + [[ -z ${CAMERON_BSKY_APP_PASSWORD:-} ]] || exit 91 + ;; +esac +EOF +chmod 0755 "$tmp/scope-bin/npm" "$tmp/scope-bin/fly" "$tmp/scope-bin/pnpm" + +PATH="$tmp/scope-bin:$PATH" \ +CAMERON_BSKY_APP_PASSWORD=scope-test-secret \ +CAMERON_SITE_REPO="$tmp/scope-source" \ +CAMERON_SITE_SYNC_STATE_DIR="$tmp/scope-state" \ + "$tmp/scope-source/scripts/sync-git-backed-content-from-origin.sh" >"$tmp/scope-run.log" + +test -f "$tmp/scope-state/last-run.json" +grep -F '"conflict": 0' "$tmp/scope-state/last-run.json" >/dev/null + +echo 'Content sync worker tests passed.'