Ordered 2026-10-16 from live Cloudflare config. Generated once, then updated by hand as batches land.
Each project gets .github/workflows/deploy.yml calling the shared workflow in cloudflare-deploy-templates. Callers are produced by scripts/render-caller.sh in that repo, which reads the live build_config — not copy-pasted. Copy-paste is what shipped strattice-consumer-branded-static-site without its required build-command.
Ordering
Ascending complexity, so the pattern is proven on the dullest projects before it reaches the ones with real build steps and secrets:
| Factor | Weight | Why |
|---|---|---|
output-dir is not src | +2 | the src default no longer applies; a wrong value publishes the wrong directory |
| has a build command | +2 | needs build-command, and the install command depends on which lockfile exists |
| has Pages Functions | +1 | bundled at deploy time, so a missing dependency fails the deploy outright |
| has secrets | +1 | must already be set on the Pages project, and cannot be read back to verify |
Disconnecting from the old git org
Correction. An earlier version of this doc claimed Cloudflare clears the git binding on the first Direct Upload deploy. Batch 1 disproved that: biocell, latisse-hcp and natrelle all deployed via Direct Upload to both dev and prod and kept an intact AA-PDCOE binding with deployments_enabled: true.
So onboarding does not disconnect the old org. The three pilots are wiped for unrelated reasons — skin-donation was already null on 3 Oct before any of this work, and skinvive/strattice lost theirs between 3 Oct and now, most plausibly during the migration fallout rather than as a result of deploying.
Practical consequence: a project can carry both an Actions pipeline and a stale git binding. Disconnecting is a separate, deliberate step — not a side effect of onboarding.
It is safe to defer. The binding is inert, not merely idle, for two independent reasons:
pages/connectionsreportshas_valid_oauth_token: false, so Cloudflare cannot fetch from GitHub at all. Zerocf_gitdeploys have landed anywhere on the account since 5 Oct.- The binding points at the old repo, not the migrated one. For
biocell-static-siteit references repo id979590192, which isAA-PDCOE/biocell-static-site; the repo we now commit to isabbvie-migrated/biocell-static-site, id1402688461. Pushes to the new repo are invisible to that binding regardless of token state.
The old AA-PDCOE repos are dormant (last pushes 3 Aug – 9 Sep, matching their final github:push deploys) but not archived, so a push there is technically possible — it just cannot reach Cloudflare.
Tracked as dual_deploy_path, downgraded to info: it is tidy-up, not risk. The risk only materialises if the GitHub App is reinstalled and someone pushes to the old org, at which point unbinding is the fix.
How to onboard one repo
Start here if you have a Cloudflare Pages repo with no deploy.yml yet. Full reference for the workflow inputs, token scoping and rotation lives in cloudflare-deploy-templates; this is just the order of operations.
0. Prerequisites, in the order they bite
cd ~/Sites/cloudflare-deploy-templates
# 1. The right GitHub account. parkpx1 CANNOT see abbvie-migrated private
# repos, and GitHub answers 404 rather than 401 -- so the wrong account
# looks like "repo is empty", not "you lack access".
export GH_TOKEN=$(gh auth token -u PARKPX_AbbVie)
gh api user -q .login # must print PARKPX_AbbVie
# 2. A Cloudflare token. Pages:Read is enough to render.
# The wrangler OAuth token works but EXPIRES HOURLY; run any wrangler
# command first to refresh it, or the script dies on an API call.
export CLOUDFLARE_API_TOKEN=<token>1. Look before you touch
DRY_RUN=1 scripts/onboard.sh <project-name>Renders the caller and changes nothing. Read the output: output-dir, exclude, build-command and install-command are all derived from the live Cloudflare config, and this is the moment to notice if any of them look wrong.
It also tells you, without failing, if the secret is missing — and skips outright if the repo already has a deploy.yml, so re-running after a partial fan-out is safe.
2. Set the deploy secret
The Deploy run fails without it, so do this before merging.
gh secret set CLOUDFLARE_PAGES_DEPLOY --repo abbvie-migrated/<repo>Repo-level, not org-level — org secrets need an org owner. Permission required is Account → Cloudflare Pages → Edit, scoped to AA-BTS. Note this is account-wide: Cloudflare has no per-project Pages scope, and the same permission also grants delete. Splitting it across repos is operational convenience, not isolation.
If an org-level secret ever replaces these, delete the repo-level copies first — GitHub silently resolves the repo secret ahead of the org one, so rotation would appear to do nothing.
3. Capture a baseline before anything deploys
This is the step that makes verification meaningful later. Record what production serves *now*:
curl -s "https://<project>.pages.dev/" \
| sed 's|<!-- Cloudflare Pages Analytics -->.*<!-- Cloudflare Pages Analytics -->||' \
| shasum -a 256Strip the analytics beacon first or the hash will never match. Baseline the real content page too if / is only a redirect stub.
That sed is line-based, so it only strips a beacon emitted on one line — which is what Cloudflare does today (verified: 214 bytes removed from strattice-consumer-branded). If a hash mismatch ever appears alongside a byte count that differs by a couple of hundred, check whether the beacon survived before assuming the content changed.
4. Open the PR
scripts/onboard.sh <project-name>Commits the caller on a branch and opens a PR against the default branch (dev). One repo per invocation, always via PR — these are live production sites.
Say in the PR body what is *not* byte-identical. A root-output project that stops publishing README.md is a visible change to a public site, and the homepage hash check cannot see it.
5. Merge to dev, verify the preview
Pushing dev publishes a preview; production is untouched until a separate dev → prod PR. Confirm:
- the
Deployrun is green - the preview HTML matches the step-3 baseline
- every Function endpoint still answers — a broken form handler does not show up in a homepage diff
- Cloudflare reports
environment: preview, notproduction
Verify in a browser, not curl — see the gotchas below.
6. Promote to prod
Open dev → prod. After merge, check that Cloudflare's canonical_deployment (not latest_deployment) is now environment: production, trigger: ad_hoc, carrying the merge SHA. That is what proves Actions owns production.
Then re-run KIWI collection and GET /api/diff — it should show prod_not_onboarded and stale_production resolved for that project and nothing introduced.
What onboarding does not do
It does not disconnect the old AA-PDCOE git binding; see Disconnecting from the old git org. A project carries both for a while, which is safe and deliberate.
Per-batch procedure
Three at a time, so a systemic mistake is caught on three sites rather than thirty.
DRY_RUN=1 scripts/onboard.sh <project>for each of the three, and read the diff- Confirm
CLOUDFLARE_PAGES_DEPLOYis set on each repo —onboard.shwarns but does not block - Open the PRs, merge to the default branch, confirm each
Deployrun is green - Verify the preview URL renders, and that any Function endpoint still answers
- Re-run KIWI collection, then
GET /api/diffto confirm exactly three projects moved out ofcannot_deployand nothing else regressed - Promote to
prodper project, on the owning team's schedule
Step 5 is the point of the diff endpoint: it turns "three PRs merged cleanly" into evidence about what actually changed in the estate.
Custom domains return 403 to scripted clients
Do not treat a 403 on a custom domain as a deploy problem. Two separate rules are in play, and neither is caused by onboarding:
- Bot mitigation challenges non-browser clients.
curlgets Cloudflare's "Attention Required" interstitial onwww.*anddev.*alike. Confirmed estate-wide against projects never touched by this work —www.botox.co.nz,www.coolsculpting.hk,global.allerganaesthetics.comall 403 identically. A real browser renderswww.*without trouble. dev.*hostnames are blocked for everyone, browser included ("Sorry, you have been blocked"). Deliberate: they are non-public staging hostnames attached to the same Pages project, DNS-active but intentionally unreachable.
www.skinvivethailand.com returning 200 to curl is the exception, not the signal — that zone's bot rule simply does not challenge the client.
Verify content via *.pages.dev instead. The bare project URL is neither WAF-challenged nor Access-gated, and comparing it byte-for-byte against the prod branch source is a stronger check than loading the custom domain.
One trap: dev.*.pages.dev aliases ARE Access-gated on some projects and not others, with no obvious pattern. A curl against a gated one returns a 143-byte 302 page, which a hash comparison reports as a content mismatch. That produced a false "DIFFER" on coolsculpting-latam-static-site during batch 4; checking in an authenticated browser showed the hash matched production exactly. Verify dev previews in the browser, or check the status code before trusting a hash.
The wrong GitHub account fails silently
render-caller.sh decides exclusions for root-output projects by probing repo paths with gh api. GitHub returns 404, not 401, for a private repo the active account cannot see — so running as parkpx1 made every probe read as "file absent" rather than erroring. beat-the-sweat-static-site's caller came out with no exclude input at all, which would have published README.md to the site root.
The script now refuses to run when it cannot read the repo. onboard.sh was never affected: it reads repo metadata upfront, which 404s and dies.
Worth internalising beyond this one script — a 404 in this org usually means *wrong account*, not *missing thing*.
A stale edge cache can hide a successful change
Removing a file from the upload does not remove it from Cloudflare's edge cache. beat-the-sweat-static-site served /README.md with its original contents for hours after the production deploy that excluded it, with cf-cache-status: HIT, age: 6131 and cache-control: public, s-maxage=604800 — a cache entry predating the deploy, with a seven-day TTL.
The deploy was correct: the run log showed Staged 243 files (excluded 2), and a cache-busted request (/README.md?cb=<timestamp>) returned the SPA fallback stub. The origin had the right content all along.
Two practical consequences:
- Append a cache-buster when verifying a removal. A bare request can return the pre-deploy object and read as "the exclusion did not work".
*.pages.devand the custom domain can disagree. Here the custom domainwww.beatthesweat-mea.comalready served the stub (cf-cache-status: DYNAMIC) while*.pages.devwas still serving the cached copy. Check both, and prefer the custom domain as the answer to "what do users see".
Purging is possible from the dashboard but was not needed: the asset is no longer referenced by the site, and the entry expires on its own.
Some projects cannot be verified by comparing files at all
device-management-portal-scanner publishes a template, not a finished page. Its 697-byte index.html contains {{SCANDIT_KEY}} and {{SCANDIT_CDN}}, and functions/index.js intercepts the request, fetches the asset through env.ASSETS and substitutes both values at request time. Served production output is 1642 bytes for that reason.
So the usual check — compare the served bytes against the branch source — reports a difference that is entirely expected, and comparing repo files proves nothing about whether the site works. The only meaningful check is the served output, with the Function running and its env vars bound. A failed substitution would render a broken scanner while still returning 200.
Before assuming a hash mismatch is a regression, check whether the project templates its HTML. Look for a Function that reads env.ASSETS, or {{...}} placeholders in the committed source.
Testing such a Function needs its real payload shape, and the site's own JS can be misleading about what that is. alloderm-hcp's /pdfdownload returned 422 Missing data to every hand-built request, which reads like a working validation path; it is actually the only thing the endpoint ever does until the body matches. sku.js appears to post {OrderDetails: [...]}, but functions/pdfdownload.js requires it nested as {_answers: {OrderDetails: [...]}}, with per-item keys ItemCode / OrderDescription / Quantity. Read the Function's own guard clause rather than inferring the contract from the caller, or a 422 will be mistaken for a pass.
A functions/ directory does not mean the project has Functions
juvederm-gr carries functions/elbeforeafter.js and enbeforeafter.js, but production reports uses_functions: False. They are Netlify handlers (exports.handler) left over from the previous platform, alongside a netlify.toml and _redirects lines pointing /el|/en/api/beforeafter/ getbeforeafter at /.netlify/functions/.... Cloudflare only recognises onRequest* exports, so on deploy wrangler logs:
WARNING No routes found when building Functions directory: .../functions - skippingand publishes no bundle. The dead endpoints keep falling through to the SPA fallback exactly as before, so onboarding is behaviour-neutral — verified by comparing all four paths on preview against production (200 / 32,840 bytes, identical).
Two consequences worth holding onto. The skip is a warning, not an error, so nothing fails if a Functions directory is silently ignored — and if anyone later converts those files to onRequest, the routes go live with no other config change, which would be a real routing change arriving via an unrelated commit. Conversely alloderm-hcp's single onRequestPost is picked up (✨ Uploading Functions bundle), so the two shapes are distinguishable only by reading the exports, not by the directory's presence.
Verifying served HTML
Some projects have Cloudflare Pages Analytics enabled, which injects a beacon <script> into HTML at serve time. On those, served HTML will never hash-match the committed source. Strip the <!-- Cloudflare Pages Analytics --> block before comparing, or the check reports a false difference — strattice-consumer-branded-static-site is one such project.
A hash on its own also cannot tell content apart from an error page. Check the status code and byte count alongside it: beat-the-sweat's 60-byte root looks broken but is a legitimate JS redirect to /en, while a 143-byte "mismatch" on a dev.* alias is an Access 302. Compare like for like — a redirect stub against a redirect stub, a content page against a content page.
Two ways this check can report a confident wrong answer:
- The hashed deployment alias is behind Access and answers
200. Verifying batch 6,ded7c704.latisse-ca-static-site.pages.devreturned 46,508 bytes of<title>Sign in ・ Cloudflare Access</title>with status200, against 6,219 bytes of real site on the project root — read as the root being stale when the root was correct. Unlike thedev.*302 above, there is no redirect to give it away. Treat a multi-kilobyte HTML body whose title mentions Access as "not logged in", not as content, and prefer the project root or custom domain for comparisons. - An empty body hashes to a constant. Two failed fetches both produce
e3b0c44298fc1c14…— the SHA-256 of zero bytes — and compare equal, so a broken command reportsIDENTICALrather than failing. Always print the byte count next to the hash; if you see that digest, nothing was downloaded. - A zone feature can rewrite HTML, so the custom domain legitimately differs from
*.pages.dev.hcp.alloderm.comserves 876 bytes more thanalloderm-hcp-static-site.pages.devbecause the zone has Email Address Obfuscation on: everymailto:becomes/cdn-cgi/l/email-protection#<hex>and acloudflare-static/email-decode.min.jstag is injected. Same class of edge mutation as the analytics beacon, but it is not marked by comment delimiters, so it cannot be stripped as cleanly. Compare*.pages.devagainst*.pages.devwhen checking a deploy, and treat a custom-domain diff made entirely ofemail-protectionlinks as benign. - A build bakes its own directory into the bundle, so two correct builds of the same commit differ. Angular embeds the absolute build path in i18n message IDs and webpack module names, so
aa-global'smain.jsis 650,276 B built by Cloudflare (/opt/buildhome/repo) and 650,528 B built by Actions (/home/runner/work/...) from identical source. Normalise every builder's path to one token before comparing — stripping only one side still reports a mismatch, which is how this first read as a content change. Normalised, three independent builders (Cloudflare, Actions, a laptop) all producemain.js45fe47d446777713andvendor.js39c5d5b1ebdd0259. Verify a build project by normalised hash, not raw bytes. - A compiler upgrade on
devchanges built output, so "dependency bump" is not automatically "no content change".harmonyca-de's promotion carriedsass 1.55.0 → 1.79.0(a Snykbracesfix) inpackage.jsononly, which looked manifest-only — butsass*is* the build, somain.min.csswent 193,975 → 196,736 B. The delta is entirely Dart Sass's colour notation: 1.55 emitted hex, 1.79 emits decimalrgb(), so--bs-table-active-bg:#373b3ebecamergb(55.2, …)and Bootstrap's embedded SVG data-URIs changedfill='%2386b7fe'tofill='rgb%28134, …. Rule count is identical (2,971{in both) and the colours are numerically equal, so nothing renders differently. Confirmed by sampling computed styles forbody,h1,h2,a,header,footerandbuttonon both environments — identical on all seven — rather than by reading the stylesheet.
The lesson is where to look. A bumped dependency is harmless only if it is not the build toolchain: juvederm-de's axios bump is a *runtime* dep and could have been bundled by vite build, yet assets/js/index.js stayed byte-identical at 69,290 B. Check which of the two you have, then compare the built asset, not the manifest.
Validation that cannot pass before the build
Template v1.2.2 asserted that output-dir exists and is non-empty in a Validate inputs step immediately after checkout. For a project whose output directory is *produced* by its build, that is a post-build condition checked pre-build, and the run dies before building:
##[error]output-dir 'dist/AGN-Corporate-Static-AAGlobalLanding' does not exist in ..
3. Validate inputs -> failure
6. Build -> skippedBatches 1–7 never caught it because all 20 of those projects commit their output directory (src or .), so it exists at checkout. aa-global was the first caller where it is a build artifact. v1.2.3 splits the step: Validate inputs keeps only what can be true pre-build (output-dir non-empty), and the directory and file-count assertions move to Validate output, after Build and before Stage assets. The guard is preserved rather than relaxed — a wrong output-dir still refuses to publish a blank site.
Two things follow. harmonyca-de (public) and juvederm-de (dist) in batch 9 have the same shape and would fail identically on v1.2.2; they are unblocked because render-caller.sh derives the newest v*.*.* tag rather than hardcoding one, so new callers pick up v1.2.3 automatically. And cutting a tag makes every already-onboarded repo drift: snapshot 16 reported template_version_drift on 19 projects at once, because the rule compares each caller's pin against the newest tag. That is the rule working, not a regression — but expect a wave of it after any release, and do not read it as something the latest batch broke.
Progress
Live, from the latest snapshot: the catalog for per-project state and the overview for the counts.
Deliberately not restated here. The table that used to sit in this section, and the per-project Batches table below it, both duplicated data the collector already reads from Cloudflare and GitHub — and the duplicates had drifted. Every one of the 24 project rows disagreed with the catalog on the domain count.
That turned out to be worse than a stale number. The doc counted *custom* domains; the collector counted the raw domains array, which includes the project's own *.pages.dev hostname. Both were defensible — biocell really does have ['biocell-static-site.pages.dev', 'www.biocellinformation.com'] — so "1" and "2" were each correct under a definition that was written down nowhere. Two columns measuring different things under one name is a worse failure than one being out of date, because nothing on the page tells you which is in force.
The columns that only existed here are now collected, so the catalog carries them as a single Build shape column: output directory, plus a build, fns or N vars pill where applicable. They all come from the pages/projects list response the collector already fetches, so this costs no extra API calls.
One definition changed in the move, for the better. The old fns column counted files in functions/; the catalog reports whether the deployment production actually serves bundles Functions. juvederm-gr-static-site shows why that matters: the repo has two Function files, but its output directory is src, so functions/ is outside the uploaded tree and production bundles none. The doc said 2; the deploy-relevant answer is 0.
The Worker count is inflated by dryrun-* leftovers
n_workers was 59 at snapshot 17, but four of those are dryrun-* scripts, so the real figure is 55. They are artifacts of --dry-run style CI jobs on *-studio-static-site repos, named dryrun-<host>-<github-run-id>, and nothing deletes them: eleven distinct ones have appeared since snapshot 4, the oldest still present from 16 Oct. Several name repos that do not exist in abbvie-migrated at all (mykybella-com-studio-static-site 404s), so they outlive whatever created them.
Consequences when reading the overview. Worker growth is not evidence of new services — 52 → 56 → 58 → 59 across snapshots 12–17 is partly this. And a dryrun-* row is not something to onboard or investigate; filter the prefix out before using the count, and treat the leftovers as cleanup rather than estate.
The allergan-blog / allergan-qr Workers are a worked example of the same care. Five exist, and they are not new arrivals: allergan-blog-staging, allergan-blog-uat, allergan-qr-production and allergan-qr-staging have been present since snapshot 4; only allergan-blog is genuinely new, first seen at snapshot 14 on 27 Oct. Their dashboard_deploy findings looked like a fresh problem but were new *findings* on old Workers, surfacing as they were redeployed.
They are also now mostly fine. At snapshot 17 four of the five report deploy_method = wrangler; allergan-blog and allergan-blog-staging both resolved dashboard_deploy between snapshots 16 and 17 after being redeployed through a pipeline on 29 Oct. allergan-qr-production is the one still on dashboard, last deployed 28 Oct — that is the only remaining action here, and it is a Worker, so it is outside this Pages rollout.
Batch order and decisions
What the catalog cannot derive: which batch a project belongs to, and why a given project is parked. Current state for each of these is on the catalog.
| # | Projects | Status |
|---|---|---|
| 1 | biocell, latisse-hcp, natrelle | prod ✅ |
| 2 | revolve-consumer, strattice-consumer-unbranded, strattice-hcp | prod ✅ |
| 3 | tiff50-landing-page, coolsculpting-br, coolsculpting-hk | prod ✅ |
| 4 | coolsculpting-in, coolsculpting-latam, coolsculpting-sg | prod ✅ 20 Oct |
| 5 | coolsculpting-th, beat-the-sweat, botox-nz | prod ✅ 21 Oct (#20, #12, #61) |
| 6 | coolsculpting-jp, latisse-ca, device-management-portal-scanner | prod ✅ 27 Oct (#47, #6); jp #38 draft, do not merge — see Open decisions |
| 7 | juvederm-gr, alloderm-hcp | prod ✅ 27 Oct (#21, #58) |
| 8 | aa-global | prod ✅ 28 Oct (#29, #31) — the only real build in the estate (ng build); needed template v1.2.3, see Validation that cannot pass before the build |
| 9 | harmonyca-de, harmonyca-th, juvederm-de | prod ✅ 29 Oct (#40, #35); harmonyca-th #27 dev only, do not promote — see Open decisions |
Ordering rationale is in Ordering; the build shape that drives it is on the catalog.
Batch 7 was originally the three projects now split across 7 and 8, labelled "first with build steps". That was wrong, and the correction is the useful part: a non-empty build_command in the Pages config does not mean the project builds. Of the three, only aa-global does — npm run build → ng build, 20 deps and 23 devDeps, angular.json outputPath matching the Pages output dir. The other two were pre-built sites with a nominal command: juvederm-gr's build is cp _headers src && cp _redirects src, and alloderm-hcp's is echo 'No build step needed' behind an npm install. Grouping them meant an Angular build failure would have blocked two no-risk onboardings, so aa-global was split out to be piloted on its own and the later batches renumbered behind it. Read the repo's package.json scripts.build, not just the Cloudflare build_command, before judging how risky a batch is.
Open decisions
harmonyca-th— onboarded todevonly; promotion is parked, and the gap is far larger thancoolsculpting-jp's.devis 43 commits and 300 files ahead ofprod, spanning 12 May 2025 → 31 Jul 2026, including a site revamp:index.htmlis rewritten (+745/−533), with 219 files underwp-content/and commits titled "New Harmonyca TH" and "indexEdited".
The pipeline itself is proven working, which is what makes the park a content decision rather than a technical one. The preview serves dev's source exactly (106,510 B, b8f0c064390f456b) and production serves prod's (109,810 B, bec7085669d2c19e) — each matches its own branch byte for byte, so the only thing a promotion would change is *which content is live*. Its functions/api/icl-locator.js also returns byte-identical 1,365,503 B JSON on both environments.
Needs a content owner's sign-off, tracked separately from this rollout. It also carries 8 project environment variables, so there is runtime surface that cannot be checked from the repo. A pre-existing console error is present identically in production: a mirrored Google Fonts stylesheet answers text/html, so the browser refuses it.
coolsculpting-jp— onboarded todevonly; promotion is parked. It is the first project in the rollout wheredevis not a content no-op: it carries 4 commits unshipped since 8 Jul 2026, including a change to the reCAPTCHA condition in its contact-form Function.
``js // prod (live today) recaptchaVerification: ENV === 'dev' ? recaptchaVerification : DEV_CAPTCHA // dev (would ship on promotion) recaptchaVerification: ENV === 'production' ? recaptchaVerification : DEV_CAPTCHA ``
The ternary forwards the real user token when true and a canned DEV_CAPTCHA when false. If production's ENV is 'production', production is not verifying captcha on a live form and the dev change is the fix; if it is 'dev', production is fine and shipping the change would break it. ENV is a secret on both Pages environments, so the value is unreadable from the repo or the API — this needs someone with dashboard access. The diff also carries a stray console.log and unreviewed dependency bumps, none of which should ride along with a pipeline change.
This repo is also the only one with both dev and develop at different SHAs, with develop as the repo default. Its caller triggers on all three branches; see the generator fix in cloudflare-deploy-templates@a0d93e3.
strattice-consumer-branded— dev green since 13 Oct, promotion PR #21 awaiting med-legal sign-off. It carries AAGSB-1393 claim changes (10→15 years, 100→190 publications, 200k→300k patients, job code v3→v4), so this is a content hold, not a technical one.beat-the-sweat— resolved 21 Oct.exclude: README.md .cfignorestopped publishing two files that production previously served. They do *not* 404: this site has an SPA-style fallback, so every unmatched path returnsindex.html— a 60-byte JS redirect to/en— with status 200. An earlier version of this doc predicted a 404, which was wrong and untested. The content removal is real, but the status code is unchanged, so a status-only check proves nothing here. Verified onwww.beatthesweat-mea.com;*.pages.devneeded a cache-buster, see A stale edge cache can hide a successful change..cfignorewas dead config anyway — it listsassets/video/, which does not exist, andwrangler pages deployignores.cfignoreentirely.botox-nz— the only repo with a committedwrangler.toml. It duplicates dashboard config (pages_build_output_dir = "src",compatibility_date = "2024-01-01") and is left untouched after confirming wrangler parses it alongside an explicit directory argument. The duplicated source of truth and the stale compatibility date are worth reconciling separately.coolsculpting-sg— its Function proxies to a per-deployment hashed host,00850a3f.functions-ciz.pages.dev/icl, where its siblings usenetlify-common-functions.pages.dev/icl. Pre-existing, not caused by onboarding, and it breaks silently if that deployment is deleted. Tracked as a separate ticket.
Not onboarded
Batches 1–9 are complete. Six projects remain, and they are not one group — lumping them together as "cannot proceed" hides the fact that most are waiting on a person, not on engineering. Status legend, used on the catalog too:
| Tag | Meaning | Who unblocks it |
|---|---|---|
| 🟡 AWAITING SIGN-OFF | pipeline built and proven on dev; only the promotion is held | a content or compliance owner |
| 🔵 NEEDS INFO | pipeline built; blocked on a fact nobody has yet | whoever has dashboard access |
| 🔴 BLOCKED — TECHNICAL | cannot be onboarded as-is; needs engineering work first | this team |
| âš« OUT OF SCOPE | deliberately never onboarding | nobody; leave alone |
Nothing here is retired or deleted. All six still exist on Cloudflare and all six still serve production traffic — verified 29 Oct, every one returning 200. A project being absent from the rollout is not evidence it is dormant.
🟡 Awaiting sign-off — on dev, promotion held
Each of these has a merged deploy.yml, a green Deploy run and a verified preview. The pipeline work is done; promoting is a content decision, which is why none should be merged by whoever next picks this up.
| Project | Live site | Held because |
|---|---|---|
harmonyca-th-static-site | www.harmonycathailand.com | dev is 43 commits / 300 files ahead of prod including a site revamp — see Open decisions |
strattice-consumer-branded-static-site | — | AAGSB-1393 claim changes await med-legal; PR #21 |
🔵 Needs info — one unanswered question
| Project | Live site | The question |
|---|---|---|
coolsculpting-jp-static-site | — | Is production's ENV secret 'production' or 'dev'? It decides whether prod is skipping captcha verification, and the value is unreadable from the repo or API. PR #38 is a draft — see Open decisions |
This repo also exposed a collector bug, fixed in snapshot 20. The probe read .github/workflows/deploy.yml on the *default* branch only, and this is the one repo whose default is develop while its workflow lives on dev — so the catalog reported has_workflow = 0, and template_ref as blank, for a project that is onboarded and merely parked. A false negative there is worse than a gap: it makes a finished pipeline look like untouched work. The probe now falls back to dev, and the catalog correctly shows ✓ and v1.2.2. Same dev/develop root cause as the generator fix in cloudflare-deploy-templates@a0d93e3.
🔴 Blocked — technical work required first
| Project | Live site | Blocker |
|---|---|---|
coolsculpting-de-static-site | www.coolsculpting.de | Next.js 14 SSR on deprecated next-on-pages; needs a vinext/OpenNext port before any pipeline change |
skinvive-ous-gigapixel-embed | pages.dev | ~88.8k files (88,826 image tiles), over the Wrangler upload limit. Proposed: serve tiles/ from R2 through a same-origin Pages Function, with no vendor code change; on hold until the team agrees the cost of a Function call per tile and a sync step for vendor tile updates |
⚫ Out of scope — do not onboard
| Project | Live site | Why |
|---|---|---|
netlify-common-functions | pages.dev | Repo is archived (the only one here that is), so it rejects writes and the deploy secret cannot be set. Still load-bearing: six coolsculpting sites proxy its /icl endpoint, so it must not be deleted either. Retiring it means moving those six consumers first. |
Pilots
The first three, kept separate because each one taught the pattern something. Template version and current state are on the catalog.
| Project | What it taught |
|---|---|
skin-donation-static-site | Pilot; promoted to prod 20 Oct (#17). Production had been 95 days stale. Its README was fixed in #18 after an automated review cited the stale main reference to argue the workflow was wrong — a doc error that cost real review time. |
skinvive-th-static-site | First root-output project, so the first to need output-dir: . and exclude. Shipped 13 months of accumulated backlog to prod in one deploy. |
strattice-consumer-branded-static-site | The case for generating callers. Its hand-copied caller omitted build-command, and the deploy failed on an unresolved pdf-lib import — which is why render-caller.sh reads the live build_config rather than trusting a paste. Promotion still parked; see Open decisions. |