Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit b0f48842bd3bf82e765518dba51f6140f119fab9
parent fb68a6bd3d626acf5e6537e68b657044e61bfaae
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 23 Sep 2026 09:05:00 -0400

docs: preview deployments, and the branch-inference hazard that predates them

DEPLOY_CLOUDFLARE gains a Preview deployments section — what a preview is, the
alias, the three surfaces, the shared R2 bucket, and that who may open a
preview is a Cloudflare project setting that defaults to public. It also
records the hazard the feature made visible rather than created: a production
deploy passes no --branch, so wrangler infers one from the checkout and a
"production" deploy from a feature branch has always landed as a preview.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
MDEPLOY_CLOUDFLARE.md | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/FACTS.md | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 139 insertions(+), 0 deletions(-)

diff --git a/DEPLOY_CLOUDFLARE.md b/DEPLOY_CLOUDFLARE.md @@ -8,6 +8,7 @@ downloads can't be abused to drive up your bill**. Everything here fits inside Cloudflare's **free tier**. - [How archives are served](#how-archives-are-served) +- [Preview deployments](#preview-deployments) - [One-time R2 setup](#one-time-r2-setup) - [Securing downloads against cost-abuse](#securing-downloads-against-cost-abuse) - [Cost expectations](#cost-expectations) @@ -48,6 +49,65 @@ deploy already uses. --- +## Preview deployments + +A **preview** is the same built bundle deployed to a branch that is not the Pages +project's production branch. Cloudflare publishes it at a **branch alias** — + +``` +https://<branch>.<project>.pages.dev +``` + +— and leaves the live site alone. Each deploy also gets an immutable +per-deployment URL (`https://<hash>.<project>.pages.dev`), which wrangler prints as +"Deployment complete! Take a peek over at …"; the editor repeats both on a +`[preview]` line at the end of the job log, because the streamed log scrolls. + +The alias is a function of the project and the branch and nothing else, so it is +known *before* the deploy runs — which is why the editor can link it while you are +still typing the name. + +**Branch names** must be 1–28 lowercase letters, digits and dashes, starting and +ending with a letter or digit. That is exactly what Cloudflare's alias sanitizer +preserves verbatim, so the alias shown is the alias that resolves. `main`, `master` +and `production` are refused: a deploy to the production branch is not a preview, it +is the live site. + +### The three surfaces + +| Surface | How | +|---|---| +| Editor | A site's **Publish** tab → *Individual steps* → **Deploy a preview**: type a branch, press **Deploy preview**. The production button beside it says **Deploy to production**. | +| Ops API | `POST /api/ops/deploy-site` `{ "siteId": "...", "preview": "<branch>" }` — deploy-only, of the already-built `export/out`. `POST /api/ops/build-deploy` takes `preview` too (build *then* preview-deploy). Both answer with `previewUrl`. | +| CLI | `pnpm ops deploy-site --json '{"siteId":"anilyzer","preview":"tags-exclude"}' --wait` — the alias is printed on its own line after the log. | + +The high-value loop is **build once, preview, then promote**: `build-site` (or the +Publish tab's *Build static export*), then `deploy-site` with a `preview`, look at +it, then `deploy-site` again with no `preview` — the same `export/out`, unrebuilt. + +### Two things to know + +**A preview shares the production R2 archive bucket.** R2 has no per-branch +namespace, and the keys are `<siteId>/archives/<file>.zip` either way. In practice +this is cheap and harmless — the upload skips any object R2 already holds at the same +size, and an unchanged channel re-zips byte-stable — but a *changed* archive +replaces the one production's manifest links to. The editor says so once at the top +of every preview deploy. + +**Who can open a preview is a Cloudflare setting, not ours.** Pages projects have a +*preview deployment access* setting (Settings → General): **public** by default, or +restricted to Cloudflare Access. A default-configured project's preview URL is +world-readable by anyone who has the link. + +> **A production deploy inherits the checkout's git branch.** `wrangler pages deploy` +> with no `--branch` infers one from the git repository it runs in, so running a +> *production* deploy from a feature-branch checkout silently produces a preview +> instead. This predates the preview feature and is unchanged: only the preview path +> passes `--branch`. If a "production" deploy did not go live, check what branch the +> editor's checkout is on. + +--- + ## One-time R2 setup ### 1. Create the bucket diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -5269,3 +5269,82 @@ checkout"). Verified 2026-09-22 on `main` @ `766e0873`: - What remains true and is worth keeping: a fresh `git worktree` still needs a composed fixture site copied into its `export/public`, or the export webServer 500s and Playwright dies at the 120 s `config.webServer` timeout. That was always a separate problem. + +## Cloudflare Pages preview deployments (added 2026-09-23, branch `deploy/preview`) + +**Every deploy this app ran before today was production, and not because it said +so.** `runDeployIntoLog` ran `pnpm dlx wrangler pages deploy <outDir> --project-name +<project>` with **no `--branch`**, and wrangler infers the branch from the git +checkout it runs in. Cloudflare Pages treats a deploy to the project's production +branch as production and any other branch as a **preview**. So the pre-existing +hazard is real and is unchanged: a "production" deploy run from a feature-branch +checkout silently lands as a preview. Only the preview path passes `--branch`. + +**The rules live in `common/lib/pagesDeploy.ts`, which is node-free on purpose.** +Four pure functions — `previewBranchProblem`, `pagesDeployArgs`, `previewAliasUrl`, +`deploymentUrlIn` — because the same rule is needed by the server action, by the ops +route (which must 400 before any job starts) and by the browser control that greys +its button out as you type. Tests: `common/lib/pagesDeploy.test.ts` (14). + +- **A valid branch is `/^[a-z0-9](?:[a-z0-9-]{0,26}[a-z0-9])?$/`, max 28 chars** + (`MAX_PREVIEW_BRANCH`). That is exactly what Cloudflare's alias sanitizer keeps + verbatim, which is what makes `previewAliasUrl` — shown in the UI *before* the job + ends — the alias that actually resolves. Uppercase is **refused, never + lowercased**: silently rewriting `Main` into `main` would deploy to production. +- **`main`, `master`, `production` are refused** with "…is the production branch; a + preview needs another name." +- **`pagesDeployArgs` with no `previewBranch` is byte-identical to the old argv.** + Pinned by a test; the production path was not to change. + +**The branch alias is `https://<branch>.<project>.pages.dev` and is knowable before +the deploy.** The immutable per-deployment URL (`https://<hash>.<project>.pages.dev`) +is only knowable after — wrangler prints it on its "Take a peek over at …" line, which +`deploymentUrlIn` scrapes off the stream as it goes past (not by re-reading the log +file). On exit 0 the job log gets ONE line: `[preview] <alias> (this deployment: +<hash url>)`, or `[deployed] <hash url>` for production. + +**A preview shares the production R2 archive bucket.** R2 has no per-branch +namespace and the keys are `<siteId>/archives/<file>.zip` either way. Cheap in +practice (the upload skips any object R2 already holds at the same size), but a +*changed* archive replaces the one production's manifest links to. +`PREVIEW_SHARES_ARCHIVES_NOTICE` (`editor/app/sites/lib/buildDeployCore.ts`) is +logged once at the top of every preview deploy, by both actions. + +**Three surfaces, one rule.** `deployExportAction(siteId, { previewBranch })` and +`buildAndDeployAction(siteId, skipArchives, { previewBranch })` each call +`previewBranchProblem` and return `{ ok: false, error }` — build-and-deploy checks +*before* the build, so a typo does not cost one. **Job kinds are unchanged** +(`deploy-export`, `build-deploy`): a preview is not a different operation. + +**`/api/ops/deploy-site` is new — deploy-only, of the already-built `export/out`.** +`POST { siteId | siteIds, preview? }`, answering build-deploy's shape plus +`previewUrl` per job (and at top level when exactly one job). It exists because +previewing is only useful if the SAME bundle can then go to production unrebuilt, +and `build-deploy` always builds. `build-deploy` gained `preview` too, but **refuses +it alongside `all`**: the all-sites runner has nowhere to thread a branch, and +building every site to ship it to production is the worst reading of the request. + +**The fan-out loop is `fanOutSiteJobs` in `editor/app/api/ops/_lib.ts`**, shared by +both deploy routes — extracted, not copied, because the careful parts (one site's +throw not costing the others their job ids; zero jobs being a 400 rather than a +cheerful `{ ok: true, jobs: [] }` that `--wait` exits 0 on) are exactly what a copy +gets wrong. `optPreviewBranch` is the body reader; it is the same +`previewBranchProblem`, asked earlier. + +**CLI: `pnpm ops deploy-site --json '{"siteId":"x","preview":"<branch>"}' [--wait]`.** +`previewUrlsIn` (exported for tests) reads the alias off `jobs[]`, falling back to the +top-level key, de-duplicated — the single-job response repeats it in both places. +Printed on **stderr**, last (after `--wait`'s logs), so `pnpm ops … | jq` still sees +nothing but the response. + +**No e2e spec starts a deploy job, deliberately.** Every external binary the suite +touches has a fake in `editor/e2e/fixtures/bin`; **wrangler has none**, because until +previews no spec had a reason to reach a deploy. So `ops-api.spec.ts` asserts the half +decided before a job exists and proves it by watching the `.jobs/*.meta.json` sidecars +not appear; a valid preview is shown to reach the action by the refusal it gets there +("no Cloudflare Pages project"), which is distinguishable from both "unknown key" and +a branch complaint. `site-publish-preview.spec.ts` covers the control itself. + +**Who can open a preview is a Cloudflare project setting, not ours** — *preview +deployment access*, **public by default**. Documented in `DEPLOY_CLOUDFLARE.md` → +"Preview deployments".