02-sops

rdco web app deployment standard

Sat Apr 25 2026 20:00:00 GMT-0400 (Eastern Daylight Time) ·sop ·status: active
deploymentcloudflare-pagesgithub-actionsstandardizationdevops

RDCO Web-App Deployment Standard

The standard pattern for deploying every Ray Data Co web property — Sanity Check (sc.raydata.co), Squarely (squarely-web), the eventual ray-data-co marketing rebuild, hq.raydata.co, and any future RDCO web app.

The pattern in one sentence

GitHub Actions builds + deploys to Cloudflare Pages on push to main, using CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID as repo-level Secrets. No local CF dependency. No 1Password lookup at deploy time. No founder-laptop-must-be-awake dependency.

Why this pattern

The canonical workflow file

Located at .github/workflows/deploy.yml in every web-app repo. Source of truth: rdco-sc deploy.yml.

Standard structure (per-app changes ONLY in the marked spots):

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - run: bun install --frozen-lockfile

      # Image optimization step — keep verbatim, threshold 50KB. WebP at quality 80.
      - name: Install cwebp
        run: |
          if ! command -v cwebp &>/dev/null; then
            sudo apt-get update -qq && sudo apt-get install -y -qq webp
          fi
      - name: Optimize images (PNG/JPG → WebP)
        run: |
          # ... (see canonical file for full bash) ...

      - name: Build
        env:
          # APP-SPECIFIC: env vars the build needs at compile time, sourced from Secrets.
          # SC needs PUBLIC_TURNSTILE_SITE_KEY. Squarely currently needs none.
          # PUBLIC_TURNSTILE_SITE_KEY: ${{ secrets.PUBLIC_TURNSTILE_SITE_KEY }}
        run: bun run build

      - name: Compute CF Pages branch alias
        id: alias
        run: |
          BRANCH="${GITHUB_REF_NAME}"
          SAFE=$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' | tr '/_' '-' | sed 's/[^a-z0-9-]//g')
          echo "branch=$SAFE" >> "$GITHUB_OUTPUT"

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          # APP-SPECIFIC: --project-name matches the CF Pages project name.
          # Convention: project name == GitHub repo name.
          command: pages deploy dist --project-name=<APP-NAME> --branch=${{ steps.alias.outputs.branch }}

Per-app variation (only these are allowed to differ)

  1. --project-name — must match the Cloudflare Pages project name. Convention: same as the GitHub repo name (e.g. sc-raydata-co, squarely-web).
  2. Build env vars — only the app-specific build-time secrets. SC needs PUBLIC_TURNSTILE_SITE_KEY. Squarely doesn’t (yet). The eventual ConvertKit form-action wiring may need PUBLIC_CONVERTKIT_FORM_ID for Squarely.
  3. Build command — currently always bun run build. If a repo uses a different package manager, change here. Otherwise leave alone.

Everything else stays verbatim across repos. If you find yourself wanting to vary something else, file an SOP-amendment proposal first — drift is the enemy.

Required GH repo Secrets

Every repo using this pattern must have these two Secrets set in Settings → Secrets and variables → Actions:

Secret nameSourceNotes
CLOUDFLARE_API_TOKENCF dashboard → My Profile → API Tokens. Permissions: Account.Cloudflare Pages: Edit, Zone.Cache Purge: Purge (optional, for SC’s purge step)One token per Cloudflare account is sufficient. Rotate yearly.
CLOUDFLARE_ACCOUNT_IDCF dashboard → any zone → right sidebarStatic. 32-character hex string.

Plus any app-specific env vars listed in the workflow’s Build step.

Standard repo bootstrap checklist

When scaffolding a new RDCO web app:

  1. Create the repo under RayDataCo/<app-name> — private by default, public if it’s a marketing surface.
  2. Set the local git config inside the repo: git config user.name "Ray Ray" && git config user.email "ray@raydata.co". Otherwise commits attribute to whatever the global config or the previous repo’s local config says. This is the standard committer identity for all RDCO repos. Founder commits attribute to him via his own setup.
  3. Copy .github/workflows/deploy.yml from rdco-sc (or another existing RDCO repo) and modify ONLY the per-app variation spots noted above.
  4. In GH repo settings, paste the two Cloudflare secrets. Copy them from rdco-sc’s Settings page (founder action, one-time per repo).
  5. In Cloudflare dashboard, create the Pages project with the matching name. Don’t connect it to GitHub on the CF side — the deploy workflow handles all pushes via the API. Connecting the GH integration in CF would create a second deploy path and conflict with the workflow.
  6. Push to main → workflow fires → first deploy creates the pages.dev URL.
  7. Wire the custom domain in CF Pages (founder action, one-time per app).

Anti-patterns to avoid

Apps currently following this standard

AppRepoCF projectStatus
Sanity CheckRayDataCo/sc-raydata-cosc-raydata-co✅ live, deploys cleanly since 2026-04-25
SquarelyRayDataCo/squarely-websquarely-web (TBD by founder)🚧 workflow added 2026-04-26, awaiting founder Secrets paste + CF project create
Ray Data Co marketingRayDataCo/rdco-websiteTBDfuture
HQ dashboardRayDataCo/hq-raydata-co (private)hq-raydata-cofuture, may differ if Workers needed instead of Pages