From 28af999b1f57cd0450fdf272caf382066e44cc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Javier=20Garc=C3=ADa=20L=C3=B3pez?= <6173685+LaloLalo1999@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:12:32 -0600 Subject: [PATCH 1/2] fix(AGE-64): make codecolony.online apex serve the landing --- .github/workflows/cd-prod.yml | 11 +++++- alchemy/dns.ts | 13 +++++-- alchemy/landing.ts | 7 ++-- scripts/attach-pages-domain.sh | 65 ++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100755 scripts/attach-pages-domain.sh diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml index 99d3f15..d60f60c 100644 --- a/.github/workflows/cd-prod.yml +++ b/.github/workflows/cd-prod.yml @@ -82,10 +82,19 @@ jobs: - name: Deploy Cloudflare infrastructure run: npm run iac:deploy -- --stage prod + - name: Attach apex domain to Pages project + run: | + chmod +x scripts/attach-pages-domain.sh + ./scripts/attach-pages-domain.sh codecolony.online + + - name: Verify apex domain + run: | + sleep 5 + curl -sS -o /dev/null -w 'HTTP %{http_code}\n' https://codecolony.online/ || true - if: always() && env.SLACK_WEBHOOK_URL != '' name: Notify Slack run: | curl -X POST -H 'Content-type: application/json' \ --data "{\"text\":\"Code Colony prod deploy finished with status: ${{ job.status }}\"}" \ - "$SLACK_WEBHOOK_URL" + "$SLACK_WEBHOOK_URL" \ No newline at end of file diff --git a/alchemy/dns.ts b/alchemy/dns.ts index 473f751..606b883 100644 --- a/alchemy/dns.ts +++ b/alchemy/dns.ts @@ -52,10 +52,17 @@ export async function dnsStack( `verify delegation before redeploying.`, ); } + const zoneId = zone.id; + void zoneId; - void zone.id; - - // Landing apex/www: deliberately not managed by alchemy. + // Landing apex/www: deliberately not managed by alchemy DnsRecords. + // Cloudflare's zone-create flow auto-provisioned `@`/`www` CNAME records + // (standard zone bootstrap), and recreating them collides with code 81053. + // Instead, the apex domain is attached as a custom domain to the Pages + // project (code-colony-landing) via the Cloudflare API in cd-prod.yml. + // Cloudflare then manages the apex DNS record and TLS automatically. + // See scripts/attach-pages-domain.sh for the API call. + void args.landing; if (args.app.hostname) { await CustomDomain("app", { diff --git a/alchemy/landing.ts b/alchemy/landing.ts index d1e2bd3..eb939b3 100644 --- a/alchemy/landing.ts +++ b/alchemy/landing.ts @@ -5,9 +5,10 @@ * still deployed by the Pages workflow outside this Alchemy stack. * * Alchemy's currently installed Cloudflare provider does not expose a Pages - * resource. DNS therefore CNAMEs the apex subdomain to the stable Pages - * hostname returned here, while previews keep using Cloudflare's default - * Pages/Workers preview URLs. + * resource. The apex domain (codecolony.online) is attached as a custom domain + * to the Pages project via the Cloudflare API in cd-prod.yml. Cloudflare then + * manages the apex DNS record and TLS automatically. + * Previews keep using Cloudflare's default Pages/Workers preview URLs. */ import type Alchemy from "alchemy"; diff --git a/scripts/attach-pages-domain.sh b/scripts/attach-pages-domain.sh new file mode 100755 index 0000000..4c1cd86 --- /dev/null +++ b/scripts/attach-pages-domain.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Attach codecolony.online as a custom domain to the Cloudflare Pages project +# code-colony-landing. Idempotent: skips if the domain is already attached. +# +# Environment: +# CLOUDFLARE_ACCOUNT_ID (required) +# CLOUDFLARE_API_TOKEN (required) +# +# This is used by cd-prod.yml because Alchemy's Cloudflare provider does not +# expose a Pages resource, so we attach the apex domain directly via the +# Cloudflare API. Once attached, Cloudflare manages the apex DNS record +# automatically (proxying + TLS). + +set -euo pipefail + +DOMAIN="${1:-codecolony.online}" +PROJECT="code-colony-landing" +ACCOUNT_ID="${CLOUDFLARE_ACCOUNT_ID:?CLOUDFLARE_ACCOUNT_ID is required}" +TOKEN="${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN is required}" + +API_BASE="https://api.cloudflare.com/client/v4" +AUTH_HDR="Authorization: Bearer ${TOKEN}" + +echo "==> Checking existing domains on Pages project '${PROJECT}' ..." + +EXISTING=$(curl -sS -H "${AUTH_HDR}" \ + "${API_BASE}/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT}/domains" \ + | jq -r '.result[]?.domain // empty') + +if echo "${EXISTING}" | grep -qxF "${DOMAIN}"; then + echo "==> Domain '${DOMAIN}' is already attached to '${PROJECT}'. Nothing to do." + exit 0 +fi + +echo "==> Attaching '${DOMAIN}' to Pages project '${PROJECT}' ..." + +RESPONSE=$(curl -sS -H "${AUTH_HDR}" -H "Content-Type: application/json" \ + -X POST \ + "${API_BASE}/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT}/domains" \ + -d "{\"name\":\"${DOMAIN}\"}") + +SUCCESS=$(echo "${RESPONSE}" | jq -r '.success') + +if [ "${SUCCESS}" != "true" ]; then + ERRORS=$(echo "${RESPONSE}" | jq -r '.errors[]?.message // .errors // empty') + # If the error says the domain already exists, treat as success + if echo "${ERRORS}" | grep -qi "already exists\|already added\|duplicate"; then + echo "==> Domain '${DOMAIN}' already exists (race or previous run). Treating as success." + exit 0 + fi + echo "ERROR: Failed to attach domain '${DOMAIN}': ${ERRORS}" >&2 + echo "Full response: ${RESPONSE}" >&2 + exit 1 +fi + +echo "==> Domain '${DOMAIN}' attached successfully." +echo "==> Cloudflare will provision the apex DNS record and TLS certificate automatically." + +# Verify by listing domains again +echo "==> Current domains on '${PROJECT}':" +curl -sS -H "${AUTH_HDR}" \ + "${API_BASE}/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT}/domains" \ + | jq -r '.result[]?.domain // empty' + +exit 0 From 48701afeafc4c227feb2d855103f82dec59ee443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Javier=20Garc=C3=ADa=20L=C3=B3pez?= <6173685+LaloLalo1999@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:52:06 -0600 Subject: [PATCH 2/2] fix(AGE-64): codex review feedback (verify --fail, idempotency parses .name + status) - attach-pages-domain.sh: parse .name (canonical Pages API field) AND inspect .status before short-circuiting; only treat active/pending/etc. as healthy. Re-attaches if status is unknown/broken. - cd-prod.yml verify step: use curl --fail-with-body, retry 6x with 15s sleep, exit non-zero on persistent 4xx/5xx so a real outage fails the workflow. - Skipped: 'duplicate Slack notification step' \u2014 only one Slack step exists; codex false positive. --- .github/workflows/cd-prod.yml | 18 +++++++++++++++--- scripts/attach-pages-domain.sh | 32 ++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml index d60f60c..6f9906b 100644 --- a/.github/workflows/cd-prod.yml +++ b/.github/workflows/cd-prod.yml @@ -88,10 +88,22 @@ jobs: ./scripts/attach-pages-domain.sh codecolony.online - name: Verify apex domain + # First Pages deploy of a new custom domain can take 30-90s for + # Cloudflare to issue the TLS cert; retry briefly. --fail-with-body + # makes curl exit non-zero on 4xx/5xx so the workflow surfaces real + # outages. run: | - sleep 5 - curl -sS -o /dev/null -w 'HTTP %{http_code}\n' https://codecolony.online/ || true - + for attempt in 1 2 3 4 5 6; do + if curl --fail-with-body -sS -o /dev/null \ + -w 'HTTP %{http_code} (attempt '$attempt')\n' \ + https://codecolony.online/; then + exit 0 + fi + echo "Apex not ready yet, sleeping..." >&2 + sleep 15 + done + echo "::error::Apex codecolony.online never returned 2xx after 6 attempts." >&2 + exit 1 - if: always() && env.SLACK_WEBHOOK_URL != '' name: Notify Slack run: | diff --git a/scripts/attach-pages-domain.sh b/scripts/attach-pages-domain.sh index 4c1cd86..b54b3e8 100755 --- a/scripts/attach-pages-domain.sh +++ b/scripts/attach-pages-domain.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Attach codecolony.online as a custom domain to the Cloudflare Pages project -# code-colony-landing. Idempotent: skips if the domain is already attached. +# code-colony-landing. Idempotent: re-attaches/repairs if the domain is present +# but not in a healthy verified state. # # Environment: # CLOUDFLARE_ACCOUNT_ID (required) @@ -23,13 +24,28 @@ AUTH_HDR="Authorization: Bearer ${TOKEN}" echo "==> Checking existing domains on Pages project '${PROJECT}' ..." -EXISTING=$(curl -sS -H "${AUTH_HDR}" \ - "${API_BASE}/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT}/domains" \ - | jq -r '.result[]?.domain // empty') +# Fetch the existing domain entry once and inspect both name (canonical) and +# status. The API response field is `.result[].name`; older docs reference +# `.domain`, but the live shape is `name`. We accept either to be safe. +EXISTING_JSON=$(curl -sS -H "${AUTH_HDR}" \ + "${API_BASE}/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT}/domains") + +EXISTING_ENTRY=$(echo "${EXISTING_JSON}" | jq -c \ + --arg dom "${DOMAIN}" \ + '.result[]? | select((.name // .domain) == $dom)') -if echo "${EXISTING}" | grep -qxF "${DOMAIN}"; then - echo "==> Domain '${DOMAIN}' is already attached to '${PROJECT}'. Nothing to do." - exit 0 +if [ -n "${EXISTING_ENTRY}" ]; then + STATUS=$(echo "${EXISTING_ENTRY}" | jq -r '.status // .verification_data.status // "unknown"') + echo "==> Domain '${DOMAIN}' already attached (status=${STATUS})." + case "${STATUS}" in + active|pending|verifying|ssl_pending|deployment_pending|initializing) + echo "==> Status is healthy/in-progress. Nothing to do." + exit 0 + ;; + *) + echo "==> Status '${STATUS}' is not healthy; will attempt re-attach." + ;; + esac fi echo "==> Attaching '${DOMAIN}' to Pages project '${PROJECT}' ..." @@ -60,6 +76,6 @@ echo "==> Cloudflare will provision the apex DNS record and TLS certificate auto echo "==> Current domains on '${PROJECT}':" curl -sS -H "${AUTH_HDR}" \ "${API_BASE}/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT}/domains" \ - | jq -r '.result[]?.domain // empty' + | jq -r '.result[]? | "\(.name // .domain // "?") (\(.status // "?"))"' exit 0