Host

Deployment

Two hosts, two opposite DNS postures. Getting them backwards causes HTTPS failures that look like certificate problems.

The host is chosen in INTAKE, not at deploy time. A public repository with no need for previews goes to GitHub Pages. A private repository with a public site, or an owner who wants preview deployments, goes to Cloudflare Pages. The choice is recorded in the spec and the state file.

The decision, in one table
ConditionHost
Public repository, no previews neededGitHub Pages from the default branch, output docs/
Private repository, public siteCloudflare Pages from the site branch, output docs
Public repository, preview deployments wantedCloudflare Pages
Public repository, owner prefers CloudflareCloudflare Pages

The two DNS postures

The custom-domain records are opposites. For GitHub Pages the record is a CNAME to the owner's GitHub Pages hostname, set to DNS-only, the grey cloud. For Cloudflare Pages the record is created by Cloudflare and stays proxied, the orange cloud.

GitHub Pages

The site is created from a branch and the /docs path. The custom domain is read from docs/CNAME, which holds the bare hostname: no protocol, no trailing slash, one line.

The build is polled until it reports built, and then the served commit is compared with the branch head. They must match. A site that answers is not proof that it serves the commit you think it does.

The certificate has four states: none, authorization pending, authorized and approved. HTTPS is enforced only after the state is approved. Enforcing earlier fails with a certificate error, and the fix is to wait, not to retry harder.

If the certificate stays at none, it is usually because the site was created before DNS existed. The playbook's nudge clears the custom domain, waits, and sets it again, then polls until approved.

Cloudflare Pages

Cloudflare Pages needs the owner to authorise the Cloudflare GitHub app for the repository. That is an owner-only step, and an agent must not attempt to bypass it. The project settings are: no framework preset, an empty build command because the output is committed, docs as the output directory, and the site branch as the production branch.

After the owner connects it, the coordinator confirms the project read-only before attaching the custom domain: the project exists, the GitHub source is attached, the output directory is docs, and the production branch is the one in the spec. Every call is recorded with its method, path and status.

The *.pages.dev address is checked as well. If it serves the same content, that is a second proof that the deployment is healthy. If it does not resolve, that is recorded as a cosmetic finding and the project URL is checked in the dashboard; it is not treated as a deployment failure.

Preview deployments are enabled for pull request branches. They are useful for reviewing a site change before it reaches production, and they cost nothing to leave on.

The launch-branch posture

When the owner wants presentation isolation, the site launches from the site branch and nothing merges to the default branch. The default branch keeps the product exactly as it was, the site branch is the presentation artifact, and it is retained indefinitely. Any future site change goes on the same branch and redeploys.

This posture is recorded in the spec and the tracker. It is never tidied up later by merging without a new decision.

Traps that bit

Symptoms, causes and fixes from real deployments
SymptomCauseFix
The social image returns 404 on the live siteA root ignore rule excluded it and it was never committedAdd an exception, commit, redeploy, and verify with git check-ignore
The certificate stays at noneThe site was created before DNS existedClear the custom domain, wait, set it again, poll until approved
HTTPS enforcement fails with a certificate errorIt was enforced before the certificate was approvedWait for approved, then enforce
Pages serves an old commitNo rebuild after switching the sourceTrigger a build and poll the commit
The site works but the sitemap 404sThe sitemap was generated but not committedRegenerate, commit the output, push
A page is live but staleThe builder output was not regeneratedRun the builder, commit docs/, push

Verification

The deploy is verified at the served commit, not just at the URL. The root answers over HTTPS, HTTP redirects to it, every sitemap URL returns 200, the social image is 1200 by 630, and the served commit equals the expected head. The deployment id and commit are recorded in the report.

The live checks
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" http://<host>/
curl -s -o /dev/null -w "%{http_code}\n" https://<host>/og.png
curl -s https://<host>/sitemap.xml | grep -o "<loc>" | wc -l

The reusable script is scripts/verify_site.py. It checks the redirect, the root, the social image, robots and every sitemap URL, and it can run against a local docs/ directory without any network calls.