Skip to content

Cosmos Atlas — deploy playbook

Live at: cosmos.aguidetocloud.com Repo: github.com/susanthgit/cosmos-atlas CF Pages project: cosmos-atlas · subdomain cosmos-atlas.pages.dev Account ID: d42846fe2c29daf890ec57877fda5e04 DNS zone: aguidetocloud.com → zone 8bf12b5f305f7b482dafa8059a0fe384

How deploy works

npm run deploy does two things:

  1. astro build — produces dist/
  2. node deploy.mjs — direct-upload to Cloudflare Pages

The deploy script (adapted from plainai/deploy.mjs): - Walks dist/ recursively - BLAKE3-hashes each file (matches wrangler's algorithm: blake3(base64 + extension) truncated to 32 hex) - Gets an upload JWT from POST /accounts/{id}/pages/projects/cosmos-atlas/upload-token - POST /pages/assets/check-missing with all hashes — gets back the subset that aren't already cached - POST /pages/assets/upload in batches of 20 — uploads only new assets - POST /accounts/{id}/pages/projects/cosmos-atlas/deployments with the manifest + branch — creates the deployment

Token resolution: 1. CLOUDFLARE_API_TOKEN env var (use this in CI) 2. ~/.copilot/secrets/cloudflare-api-token file (local dev fallback)

Why direct-upload (not wrangler)

Wrangler's workerd dependency has no Windows-ARM64 binary as of May 2026. npm install wrangler fails with Unsupported platform: win32 arm64 LE on Sush's machine. Direct API calls work everywhere — Linux x64, macOS, Windows x64, Windows ARM64.

If we move to GitHub Actions later, we can use cloudflare/wrangler-action@v3 (which runs on Linux runners — workerd works fine there). For now, the local npm run deploy is the canonical path.

Local deploy (the canonical path)

cd C:\ssClawy\cosmos-atlas
npm run deploy

Expected output:

> astro build
> node deploy.mjs

📦 Bundling ./dist/...
  19 files
  19 assets · 0 special (none)
🔢 Hashing assets...
  19 assets hashed
🔑 Getting upload JWT...
  got JWT (969 chars)
🔍 Checking which assets Cloudflare already has...
  N of 19 need uploading (M cached)
  uploaded N / N
🚀 Creating deployment...
  ✓ deployment xxxxxxxx created
  url:        https://xxxxxxxx.cosmos-atlas.pages.dev
  stage:      queued
🌍 Live at: https://xxxxxxxx.cosmos-atlas.pages.dev

After ~30-60 seconds, the deployment finishes and: - The deploy URL serves the new build - cosmos.aguidetocloud.com (custom domain alias) also serves the new build - cosmos-atlas.pages.dev (project subdomain) serves the new build

Smoke test (always run after deploy)

# Verify HTML
curl https://cosmos.aguidetocloud.com/ -I

# Verify lotus webp serves
curl -I https://cosmos.aguidetocloud.com/planets/earth-lotus.webp

# Verify CSS bundle
$css = (Invoke-WebRequest https://cosmos.aguidetocloud.com/).Content | Select-String -Pattern 'href="(/_astro/[^"]+\.css)"' -AllMatches
$cssPath = $css.Matches[0].Groups[1].Value
curl -I "https://cosmos.aguidetocloud.com$cssPath"

# Verify a font fetch
curl -I "https://cosmos.aguidetocloud.com/_astro/space-grotesk-latin-400-normal.CJ-V5oYT.woff2"

Or just use the integrated check:

# Quick all-in-one
@('/', '/favicon.svg', '/robots.txt', '/sitemap.xml', '/planets/earth-lotus.webp') | ForEach-Object {
  $r = Invoke-WebRequest -Uri "https://cosmos.aguidetocloud.com$_" -Method Head -UseBasicParsing
  Write-Host "$($r.StatusCode)  $_"
}

Rollback

If a deploy ships a regression, two paths:

Path A — re-deploy a previous commit:

cd C:\ssClawy\cosmos-atlas
git log --oneline -5
git checkout <good-commit-sha>
npm run deploy
git checkout main  # back to main, fix the regression in a new commit

Path B — promote a previous deployment in CF Pages:

$cfToken = (Get-Content "$env:USERPROFILE\.copilot\secrets\cloudflare-api-token" -Raw).Trim()
$cfAccountId = "d42846fe2c29daf890ec57877fda5e04"
$h = @{ "Authorization" = "Bearer $cfToken"; "Content-Type" = "application/json" }

# List recent deployments
$deps = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/accounts/$cfAccountId/pages/projects/cosmos-atlas/deployments" -Headers $h
$deps.result | Select-Object -First 5 | Format-Table id, url, created_on

# Promote a previous deployment to production
$deploymentId = "<id-from-above>"
Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/accounts/$cfAccountId/pages/projects/cosmos-atlas/deployments/$deploymentId/rollback" -Method Post -Headers $h

Custom domain + DNS — already configured

Both already exist (created 8 May 2026). Reference for setup if ever needed elsewhere:

$cfToken    = (Get-Content "$env:USERPROFILE\.copilot\secrets\cloudflare-api-token" -Raw).Trim()
$cfDnsToken = (Get-Content "$env:USERPROFILE\.copilot\secrets\cloudflare-dns-token" -Raw).Trim()
$cfAccountId = "d42846fe2c29daf890ec57877fda5e04"
$zoneId = "8bf12b5f305f7b482dafa8059a0fe384"  # aguidetocloud.com
$h    = @{ "Authorization" = "Bearer $cfToken"; "Content-Type" = "application/json" }
$hDns = @{ "Authorization" = "Bearer $cfDnsToken"; "Content-Type" = "application/json" }

# 1. Add custom domain to the Pages project
$body = @{ name = "cosmos.aguidetocloud.com" } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/accounts/$cfAccountId/pages/projects/cosmos-atlas/domains" -Method Post -Headers $h -Body $body

# 2. Add CNAME record
$dnsBody = @{
  type = "CNAME"
  name = "cosmos"
  content = "cosmos-atlas.pages.dev"
  proxied = $true
  ttl = 1
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records" -Method Post -Headers $hDns -Body $dnsBody

SSL provisioning takes ~1-3 minutes after both are set up.

Build numbers (8 May 2026 baseline)

Step Time Notes
astro build ~5s static output, 1 page
node deploy.mjs (initial — 19 new) ~15s hashes + upload + finalise
node deploy.mjs (incremental — most cached) ~3s only changed files re-uploaded
Cold cache from CF edge ~120ms TLS + HTML download
Total transferred (HTML+CSS+JS+fonts+lotus) ~175 KB under 250 KB budget

Setting up GitHub Actions (future, optional)

To auto-deploy on push to main, add .github/workflows/deploy.yml:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Then in the repo settings, add a secret CLOUDFLARE_API_TOKEN with the token from ~/.copilot/secrets/cloudflare-api-token. Push to main → auto-deploy.

This is not enabled in v1 because: - The local npm run deploy is fast (~15s) and gives Sush the smoke-test feedback in his terminal - GitHub Actions adds complexity and CI minutes for a one-page static site - v2 can flip this on if the deploy cadence picks up

Incident pattern (if any)

If a deploy breaks the live site: 1. Don't debug on production. Roll back first via Path A or Path B above. 2. Investigate locally with npm run dev. 3. Re-deploy when fixed.

This atlas is not a paid product (per cosmos law #6) so there's no SLA the way Practice Exams have. But quality still matters — broken deploys to a public URL erode trust.

Changelog

Date Event
8 May 2026 First deploy. Project + custom domain + DNS all set up. SSL provisioning < 1 min. cosmos.aguidetocloud.com live.