[object Object]

← back to Coming Soon Template

deploy bhbutler+boulevardbutler as Workers; flip-script guard now checks apex DNS record (all 21 flip-ready)

a06f760a226c6a72a28bc9f75c70d459fbbf016f · 2026-05-18 12:53:47 -0700 · SteveStudio2

Files touched

Diff

commit a06f760a226c6a72a28bc9f75c70d459fbbf016f
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 18 12:53:47 2026 -0700

    deploy bhbutler+boulevardbutler as Workers; flip-script guard now checks apex DNS record (all 21 flip-ready)
---
 data/ns-flip.json                     | 20 ++++++++++++++++++--
 scripts/deploy_workers.sh             | 35 ++++++++++++++++++++++++++++++-----
 scripts/godaddy_ns_api.js             | 24 ++++++++++++------------
 workers/bhbutler/wrangler.toml        |  7 +++++++
 workers/boulevardbutler/wrangler.toml |  6 ++++++
 5 files changed, 73 insertions(+), 19 deletions(-)

diff --git a/data/ns-flip.json b/data/ns-flip.json
index 3746ab6..b8ab0a1 100644
--- a/data/ns-flip.json
+++ b/data/ns-flip.json
@@ -229,10 +229,26 @@
   },
   {
     "domain": "bhbutler.com",
-    "status": "skipped_no_project"
+    "status": "would_flip",
+    "from": [
+      "ns73.domaincontrol.com",
+      "ns74.domaincontrol.com"
+    ],
+    "to": [
+      "ezra.ns.cloudflare.com",
+      "kimora.ns.cloudflare.com"
+    ]
   },
   {
     "domain": "boulevardbutler.com",
-    "status": "skipped_no_project"
+    "status": "would_flip",
+    "from": [
+      "ns37.domaincontrol.com",
+      "ns38.domaincontrol.com"
+    ],
+    "to": [
+      "ezra.ns.cloudflare.com",
+      "kimora.ns.cloudflare.com"
+    ]
   }
 ]
\ No newline at end of file
diff --git a/scripts/deploy_workers.sh b/scripts/deploy_workers.sh
index ba44f7c..0f76d24 100755
--- a/scripts/deploy_workers.sh
+++ b/scripts/deploy_workers.sh
@@ -6,32 +6,57 @@
 #
 #   ./scripts/deploy_workers.sh
 #
-# Idempotent: re-deploying just publishes a new version.
-set -euo pipefail
+# Two steps per domain:
+#   1. wrangler deploy  — uploads the assets-only Worker. On a brand-new
+#      Worker with no route yet, wrangler exits non-zero ("register a
+#      workers.dev subdomain or specify routes") AFTER the upload succeeds;
+#      that exit is tolerated because step 2 supplies the destination.
+#   2. PUT /accounts/{acct}/workers/domains — binds the apex hostname to the
+#      Worker (account-level API; needs only Workers Scripts:Edit, not the
+#      zone-level Workers Routes scope). This also creates the zone DNS
+#      record. Idempotent: re-binding an existing domain returns success.
+set -uo pipefail
 cd "$(dirname "$0")/.."
 
 [ -f .env ] || { echo "✗ missing .env"; exit 1; }
 set -a; source .env; set +a
 export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_PAGES_TOKEN:?CLOUDFLARE_PAGES_TOKEN not set}"
+ACCT="${CLOUDFLARE_ACCOUNT_ID:?CLOUDFLARE_ACCOUNT_ID not set}"
 
 DOMAINS=(bhbutler.com boulevardbutler.com)
 
 for domain in "${DOMAINS[@]}"; do
   project="${domain%.com}"
   [ -f "out/$domain/index.html" ] || { echo "✗ no artifact: out/$domain"; exit 1; }
+
   workdir="workers/$project"
   mkdir -p "$workdir"
   cat > "$workdir/wrangler.toml" <<EOF
 name = "$project"
+account_id = "$ACCT"
 compatibility_date = "2026-05-18"
 
 [assets]
 directory = "../../out/$domain"
 EOF
-  echo "→ deploying Worker: $project  (assets: out/$domain)"
-  ( cd "$workdir" && npx --yes wrangler deploy )
+
+  echo "→ [1/2] uploading Worker: $project"
+  ( cd "$workdir" && npx --yes wrangler deploy ) || \
+    echo "   (wrangler exited non-zero — expected on a routeless Worker; upload still succeeded)"
+
+  zid=$(node -e "const z=require('./data/zone-setup.json').find(x=>x.domain==='$domain');process.stdout.write(z?z.zone_id:'')")
+  [ -n "$zid" ] || { echo "   ✗ no zone_id for $domain in data/zone-setup.json"; continue; }
+
+  echo "→ [2/2] binding $domain -> Worker '$project'"
+  resp=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCT/workers/domains" \
+    -H "Authorization: Bearer $CLOUDFLARE_PAGES_TOKEN" -H "Content-Type: application/json" \
+    --data "{\"zone_id\":\"$zid\",\"hostname\":\"$domain\",\"service\":\"$project\",\"environment\":\"production\"}")
+  if echo "$resp" | grep -q '"success":true'; then
+    echo "   ✓ $domain bound to $project"
+  else
+    echo "   ✗ bind failed: $resp"
+  fi
   echo
-  sleep 6
 done
 
 echo "✓ worker deploys done"
diff --git a/scripts/godaddy_ns_api.js b/scripts/godaddy_ns_api.js
index 28239c8..4cef9d9 100644
--- a/scripts/godaddy_ns_api.js
+++ b/scripts/godaddy_ns_api.js
@@ -48,23 +48,20 @@ async function gd(method, urlPath, body) {
   return { http: res.status, json };
 }
 
-async function cfProjects() {
-  const names = new Set();
-  for (let page = 1; page <= 10; page++) {
-    const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${ACCT}/pages/projects?page=${page}`,
-      { headers: { 'Authorization': `Bearer ${CF_TOK}` } });
-    const j = await res.json();
-    (j.result || []).forEach(p => names.add(p.name));
-    if (!(j.result || []).length || (j.result_info && page >= j.result_info.total_pages)) break;
-  }
-  return names;
+// A domain is safe to flip only if its Cloudflare zone already has an apex
+// DNS record — i.e. there is something to serve (Pages CNAME or Worker
+// custom-domain record). Flipping a zone with no record makes the site dark.
+async function hasApexRecord(zid, domain) {
+  const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zid}/dns_records?name=${domain}`,
+    { headers: { 'Authorization': `Bearer ${CF_TOK}` } });
+  const j = await res.json();
+  return (j.result || []).some(r => r.name === domain);
 }
 
 const isCfNs = ns => Array.isArray(ns) && ns.every(h => /\.ns\.cloudflare\.com$/i.test(h));
 
 async function main() {
   const zones = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/zone-setup.json'), 'utf8'));
-  const projects = await cfProjects();
   const results = [];
   console.log(APPLY ? '=== APPLYING nameserver flip ===\n' : '=== DRY RUN (use --apply to flip) ===\n');
 
@@ -74,8 +71,11 @@ async function main() {
     const ns = z.cf_ns || [];
 
     if (z.live_mx) { console.log(`HOLD  ${domain}  — live MX (${z.live_mx})`); results.push({ domain, status: 'held_live_mx' }); continue; }
-    if (!projects.has(project)) { console.log(`SKIP  ${domain}  — no Pages project (would go dark)`); results.push({ domain, status: 'skipped_no_project' }); continue; }
     if (ns.length < 2) { console.log(`SKIP  ${domain}  — zone has no cf_ns`); results.push({ domain, status: 'skipped_no_ns' }); continue; }
+    if (!z.zone_id || !(await hasApexRecord(z.zone_id, domain))) {
+      console.log(`SKIP  ${domain}  — zone has no apex DNS record (would go dark)`);
+      results.push({ domain, status: 'skipped_no_record' }); continue;
+    }
 
     const cur = await gd('GET', `/domains/${domain}`);
     const curNs = (cur.json && cur.json.nameServers) || [];
diff --git a/workers/bhbutler/wrangler.toml b/workers/bhbutler/wrangler.toml
index ea6c5f2..8c617e1 100644
--- a/workers/bhbutler/wrangler.toml
+++ b/workers/bhbutler/wrangler.toml
@@ -1,5 +1,12 @@
 name = "bhbutler"
+account_id = "a8fb08368f6f6033529265bf4b0cb1c3"
 compatibility_date = "2026-05-18"
 
 [assets]
 directory = "../../out/bhbutler.com"
+
+# Deploy straight onto the apex custom domain — no workers.dev subdomain
+# needed. custom_domain=true makes Cloudflare create the zone DNS record.
+[[routes]]
+pattern = "bhbutler.com"
+custom_domain = true
diff --git a/workers/boulevardbutler/wrangler.toml b/workers/boulevardbutler/wrangler.toml
new file mode 100644
index 0000000..7e91c78
--- /dev/null
+++ b/workers/boulevardbutler/wrangler.toml
@@ -0,0 +1,6 @@
+name = "boulevardbutler"
+account_id = "a8fb08368f6f6033529265bf4b0cb1c3"
+compatibility_date = "2026-05-18"
+
+[assets]
+directory = "../../out/boulevardbutler.com"

← e51fd58 add Worker deploy script for the 2 Pages-cap casualties  ·  back to Coming Soon Template  ·  add coming-soon fleet review viewer (21 domains + dead-domai b5bdfdf →