Skip to content

Commit 0093673

Browse files
committed
fix(worker): unblock deploys by removing the unprovisioned D1 binding
`wrangler.toml` declared a D1 binding whose `database_id` was a placeholder of all zeroes for a database that was never created. Cloudflare validates bindings when the script is uploaded, so every `wrangler deploy` failed: D1 binding 'RATINGS_DB' references database '00000000-0000-0000-0000-000000000000' which was not found. [code: 10181] That took the whole worker down with it, including the CORS data proxy, which has nothing to do with D1. The worker has been undeployable since #426 landed on 2026-06-13; production still serves pre-#426 code, and `/v1alpha` falls through to the upstream origin as a 404. Critically, `data/festivals.json` edits also trigger this deploy, so festival data changes could not reach production either. Comment the binding out rather than leave a placeholder id. `reviews.ts` already degrades to 503 UNAVAILABLE / STORAGE_UNCONFIGURED when `env.RATINGS_DB` is absent, so the review API keeps the behaviour it has had all along (it has never served traffic) while the proxy, /health and /festivals.json deploy again. Provisioning D1 stays a one-time maintainer step, now documented as uncomment-and-paste. Nothing caught this: the vitest pool and `wrangler dev` use a simulated local D1 that ignores the id, and `wrangler deploy --dry-run` — CI's `validate-worker` job — exits 0 on a dangling binding (verified against wrangler 4.98.0). The PR that broke deploys had a green CI run. So: - move the test D1 binding to `vitest.config.js`, decoupling the suite from the deployed binding set - add `test/wrangler-config.test.js`, which fails on a placeholder id and on a D1 binding reappearing without the degraded-mode tests being updated (verified red against the exact config that broke production) - add tests asserting every /v1alpha route returns 503 STORAGE_UNCONFIGURED and that /health and /festivals.json still work with no storage — the configuration actually being deployed Docs corrected across the worker README and seven skills, which described the placeholder as deploy-neutral and named `--dry-run` as the check that would catch this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E7Rma9m9unUYXYgpon6Rph
1 parent b6ef25e commit 0093673

13 files changed

Lines changed: 260 additions & 60 deletions

File tree

.claude/skills/api-contract/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,9 @@ Other implementation details worth knowing:
262262
- **`STORAGE_UNCONFIGURED` 503**: if `env.RATINGS_DB` is unbound,
263263
`handleReviews` returns `503 UNAVAILABLE`/`STORAGE_UNCONFIGURED` before
264264
touching D1 (`reviews.ts:158-166`). This is also the state of the *real*
265-
production database today: `wrangler.toml:26` has a placeholder
266-
`database_id = "00000000-0000-0000-0000-000000000000"` — D1 has not been
267-
provisioned. Tests and `wrangler dev` use a simulated local D1 via
265+
production worker today: D1 has not been provisioned, so the
266+
`[[d1_databases]]` block in `wrangler.toml` is commented out and the binding
267+
is genuinely absent. Tests and `wrangler dev` use a simulated local D1 via
268268
`@cloudflare/vitest-pool-workers`, so the whole test suite runs green
269269
without a real database. Provisioning is out of scope for this skill — see
270270
`run-and-operate` for the `wrangler d1 create` runbook, or

.claude/skills/architecture-contract/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,8 @@ found" — they're tracked.
328328
the class without checking every import, and never rename the route.
329329
- **The `/v1alpha` catalogue API is contract-only** (issue #432/PR #433):
330330
proto + generated OpenAPI + a read-only worker endpoint exist, but there
331-
is no production server backing MyFestival sync yet — D1 has a
332-
placeholder `database_id` (`cloudflare-worker/wrangler.toml:26`). See
331+
is no production server backing MyFestival sync yet — D1 is unprovisioned
332+
and its binding is commented out in `cloudflare-worker/wrangler.toml`. See
333333
`run-and-operate` for the provisioning gap and `api-contract` for the
334334
proto surface itself.
335335

.claude/skills/build-and-env/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ train.
241241
| `TEST_LOG` / `ANALYZE_LOG` | `mktemp /tmp/test-XXXXXX.log` / `/tmp/analyze-XXXXXX.log` per run (`mise-tasks/test.sh:6`, `mise-tasks/analyze.sh:6`) | the `test`/`analyze` file-tasks themselves (tee output, preserve exit code via `${PIPESTATUS[0]}`) | Same on both; override to a stable path when you need to grep the same log across multiple invocations without re-running |
242242
| `ENVIRONMENT` (wrangler `[vars]`) | `"production"` (`cloudflare-worker/wrangler.toml:6-7`) | Worker code (`worker.js`/`shared.ts`) for any environment-conditional behavior | Only one value committed — there's no separate staging `[vars]` block in `wrangler.toml`; staging behavior is driven by **origin-based** CORS/bucket logic instead (below), not this var |
243243
| `RATINGS_BUCKET` (env override) | unset — falls back to `resolveBucket(origin, env)` (`cloudflare-worker/shared.ts:23-27`): `origin === "https://cambeerfestival.app"``"prod"`, else `"test"` | `shared.ts` bucket resolution for the D1 `reviews` table's composite key | Not set anywhere in committed config today; it's an escape hatch for forcing a bucket regardless of request origin. Do not set it in production without understanding it silently overrides the origin check |
244-
| `RATINGS_DB` (D1 binding, not an env var) | `[[d1_databases]] binding = "RATINGS_DB"`, `database_id = "00000000-0000-0000-0000-000000000000"` **placeholder** (`wrangler.toml:20-25`) | `reviews.ts` via `env.RATINGS_DB`; missing/misconfigured → worker returns 503 `STORAGE_UNCONFIGURED` | Tests/local dev use wrangler's simulated local D1 (id ignored); a real deploy needs `wrangler d1 create cbf-myfestival` + paste the real id + `wrangler d1 migrations apply` — see `run-and-operate` for the provisioning runbook |
244+
| `RATINGS_DB` (D1 binding, not an env var) | `[[d1_databases]]` block **commented out** in `wrangler.toml` (a placeholder `database_id` broke every deploy — error 10181); the test-only binding lives in `vitest.config.js` (`miniflare.d1Databases`) | `reviews.ts` via `env.RATINGS_DB`; missing/misconfigured → worker returns 503 `STORAGE_UNCONFIGURED` | Tests/local dev use a simulated local D1 (id ignored; `--dry-run` doesn't check resource existence either, so `test/wrangler-config.test.js` guards the placeholder case); enabling it needs `wrangler d1 create cbf-myfestival` + uncomment the block with the real id + `wrangler d1 migrations apply --remote` — see `run-and-operate` for the provisioning runbook |
245245
| `BASE_URL` (Playwright) | `"http://127.0.0.1:8080"` (`playwright.config.ts:34`) | `test-e2e/*.spec.ts` via `page.goto`/`baseURL` | Local/CI default targets a locally-served build; CI's `smoke-test-preview` job sets `BASE_URL=<Cloudflare Pages preview URL>` to run `csp-smoke.spec.ts` against a real deployed CSP policy — the only place that check is meaningful (`web/_headers` CSP isn't exercised any other way) |
246246

247247
## 7. Adding a tool or task correctly

.claude/skills/debugging-playbook/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ most battles in this repo have already been fought once.
9292

9393
| Symptom | Likely cause | Discriminating experiment | Fix pattern | History |
9494
|---|---|---|---|---|
95-
| `/v1alpha/...` returns 503 with reason `STORAGE_UNCONFIGURED` | `env.RATINGS_DB` D1 binding missing — `cloudflare-worker/wrangler.toml` ships a placeholder `database_id = "00000000-..."` | `curl -s https://data.cambeerfestival.app/v1alpha/festivals/x/drinks/y/review -H 'X-Device-Id: t'` and check the error body (`reviews.ts:158-166`) | Provision D1: `wrangler d1 create cbf-myfestival` → paste real id into wrangler.toml → `wrangler d1 migrations apply cbf-myfestival --remote` (`--remote` migrates the real DB, not the local sim); token needs D1:Edit. Full runbook: skill `run-and-operate` | v1alpha arc (PR #426); deploy still pending |
95+
| `/v1alpha/...` returns 503 with reason `STORAGE_UNCONFIGURED` | Expected, not a bug: `env.RATINGS_DB` is absent because the `[[d1_databases]]` block in `cloudflare-worker/wrangler.toml` is **deliberately commented out** until a database is provisioned | `curl -s https://data.cambeerfestival.app/v1alpha/festivals/x/drinks/y/review -H 'X-Device-Id: t'` and check the error body (`reviews.ts:158-166`) | Provision D1: `wrangler d1 create cbf-myfestival` → uncomment the block and paste the real id → `wrangler d1 migrations apply cbf-myfestival --remote` (`--remote` migrates the real DB, not the local sim); token needs D1:Edit. Full runbook: skill `run-and-operate` | v1alpha arc (PR #426); binding disabled to unblock deploys |
96+
| `wrangler deploy` fails with `D1 binding 'X' references database '…' which was not found [code: 10181]`, while `npm test` is green | A binding names a resource that doesn't exist in the account. Cloudflare validates bindings at upload; the vitest pool and `wrangler dev` use simulated local resources and never check the id, so tests cannot catch it | **Not** `--dry-run` — it exits 0 on a dangling binding (verified, wrangler 4.98.0). Read the deploy log for the resource id, then confirm the resource exists in the account (dashboard, or `wrangler d1 list`). `cloudflare-worker/test/wrangler-config.test.js` catches the placeholder case offline | Either provision the resource, or remove/comment out the binding so the rest of the worker can deploy (the code already degrades to 503 `STORAGE_UNCONFIGURED`). Never leave a placeholder id in place: it blocks *every* deploy, proxy included | Broke all worker deploys 2026-06-13 → 2026-08-10; the live worker silently stayed on pre-#426 code for ~2 months |
9697
| Browser: CORS error calling the worker; curl works fine | Origin not in the allow-list — the worker returns NO CORS headers for unknown origins (silent reject) | Compare your page's `Origin` against `ALLOWED_ORIGINS` (`cloudflare-worker/worker.js:29-37`) and the wildcard suffixes in `getCorsHeaders` (worker.js:279-329): `*.cambeerfestival.pages.dev`, `*.staging-cambeerfestival.pages.dev`, `*.trycloudflare.com` | Add the origin to the allow-list — but `cloudflare-worker/` is on the Do-Not-Modify list; needs explicit maintainer request + PR | Allow-list design; ops trap (trycloudflare wildcard live in prod) |
9798
| Worker returns 502 | Upstream `data.cambridgebeerfestival.com` fetch failed — the worker proxies everything not matched by its own routes | `curl -si https://data.cambridgebeerfestival.com/<same-path>` — is upstream itself down/erroring? | Nothing to fix app-side; upstream CAMRA feeds are untouchable (unwritten rule). The app's SWR cache is the mitigation — verify cached data still renders with the refresh notice | worker.js:102-137 |
9899

.claude/skills/my-festival-campaign/SKILL.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,11 @@ account. `cloudflare-worker/` is on the **Do-Not-Modify list** — it needs an
330330
**explicit maintainer request**, and the **festival-freeze window** must be clear
331331
(`change-control`). Do not provision on your own initiative.
332332

333-
**Provision** (one-time — `wrangler.toml:26` `database_id` is the placeholder
334-
`00000000-...`; binding `RATINGS_DB`, db name `cbf-myfestival`). Follow the exact
335-
command sequence in skill `run-and-operate` §4 — `wrangler d1 create
336-
cbf-myfestival`, paste the returned id into `wrangler.toml`, then
333+
**Provision** (one-time — the `[[d1_databases]]` block in `wrangler.toml` is
334+
commented out, so there is no binding at all today; binding name `RATINGS_DB`,
335+
db name `cbf-myfestival`). Follow the exact command sequence in skill
336+
`run-and-operate` §4 — `wrangler d1 create cbf-myfestival`, uncomment the block
337+
and paste the returned id into `wrangler.toml`, then
337338
`wrangler d1 migrations apply cbf-myfestival --remote`. The **`--remote` flag is
338339
mandatory**: without it `wrangler` migrates only the local simulated D1 and the
339340
real production database is left unmigrated while the command reports success.
@@ -359,8 +360,11 @@ curl -sS -X PATCH \
359360
curl -sS https://data.cambeerfestival.app/v1alpha/festivals/cbf2025/reviewSummaries/beer-1
360361
# expect: {"name":"...","ratingCount":>=1,"averageRating":...,"recommendRate":...}
361362
```
362-
- **See 503 after provisioning?** The binding didn't resolve — the `database_id`
363-
paste is wrong or the deploy predates the migration. Re-apply, redeploy.
363+
- **See 503 after provisioning?** The binding didn't resolve — the block is
364+
still commented out, the `database_id` paste is wrong, or the deploy predates
365+
the migration. Note `--dry-run` lists the binding
366+
even when the database does not exist, so it proves nothing here — check the
367+
real deploy log. Re-apply the migration and redeploy.
364368
- **See `test` data leaking to `prod`?** Bucket resolution keys on `Origin`; only
365369
`https://cambeerfestival.app``prod`, everything else → `test` (or the
366370
`RATINGS_BUCKET` var). That's correct isolation, not a bug.
@@ -573,7 +577,7 @@ Written 2026-07-02. Verified against the working tree at that date:
573577
- Worker: `Review` API (`/review`, `/reviews`, `/reviewSummaries`) implemented in
574578
`cloudflare-worker/reviews.ts`; 503 `STORAGE_UNCONFIGURED` guard on
575579
`env.RATINGS_DB`; `X-Device-Id` identity; bucket resolution in `shared.ts`;
576-
placeholder `database_id` in `wrangler.toml:26`; migration
580+
D1 binding commented out in `wrangler.toml` (unprovisioned); migration
577581
`0001_create_reviews_table.sql`; curl set + provisioning in
578582
`cloudflare-worker/README.md`. The `DrinkEntry`/`drinkEntries` proto endpoints
579583
are **not** implemented (grep over `cloudflare-worker/*.ts`).
@@ -598,8 +602,8 @@ rg -n 'favorites' lib/router.dart
598602
# Current goldens on disk
599603
find test -path '*goldens*' -name '*.png'
600604

601-
# D1 still a placeholder?
602-
rg -n 'database_id' cloudflare-worker/wrangler.toml
605+
# D1 still unprovisioned? (expect: [[d1_databases]] commented out)
606+
rg -n 'd1_databases' -A5 cloudflare-worker/wrangler.toml
603607

604608
# Baseline
605609
./bin/mise run check # add MISE_ENV=claude-code-web on a 403-sandboxed box

.claude/skills/reference/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ are at different maturity levels and it's easy to conflate them.
273273
| Surface | Status | Where implemented | What it does |
274274
|---|---|---|---|
275275
| **Static beverage feeds** | Live in production, untouchable upstream data | `data.cambridgebeerfestival.com` (CAMRA's own static files) proxied through `cloudflare-worker/worker.js` at `data.cambeerfestival.app/{festivalId}/{category}.json`; also `/festivals.json` (embedded registry, `no-cache, must-revalidate`) and `/{festivalId}/available_beverage_types.json` (scrapes an upstream Apache directory listing, 1h cache) | The entire catalogue the app renders — Producers→Products, per festival per category (§3). This is what `BeerApiService`/`FestivalService` fetch. |
276-
| **v1alpha Review API** | Live code, deployed worker, **D1 database not yet provisioned** (`wrangler.toml` ships a placeholder `database_id`) | `cloudflare-worker/reviews.ts` + `shared.ts`, routed at `/v1alpha/...` inside the same worker; D1 table `reviews` (`cloudflare-worker/migrations/0001_create_reviews_table.sql`) | Anonymous star-rating + "would recommend" reviews, keyed by `X-Device-Id` header (not signed-in identity yet — `user_id` column reserved for a future sign-in upgrade). GET/PATCH/DELETE one review, list caller's reviews, get/list aggregate summaries per drink. Any unmatched `/v1alpha/*` path 404s — it is never proxied upstream. |
276+
| **v1alpha Review API** | Live code, deployed worker, **D1 database not yet provisioned**, so `/v1alpha` answers 503 `STORAGE_UNCONFIGURED` in production (the `[[d1_databases]]` block in `wrangler.toml` is commented out) | `cloudflare-worker/reviews.ts` + `shared.ts`, routed at `/v1alpha/...` inside the same worker; D1 table `reviews` (`cloudflare-worker/migrations/0001_create_reviews_table.sql`) | Anonymous star-rating + "would recommend" reviews, keyed by `X-Device-Id` header (not signed-in identity yet — `user_id` column reserved for a future sign-in upgrade). GET/PATCH/DELETE one review, list caller's reviews, get/list aggregate summaries per drink. Any unmatched `/v1alpha/*` path 404s — it is never proxied upstream. |
277277
| **proto CatalogService / MyFestivalService** | **Paper contract only** — defines the intended future v1alpha REST surface via `google.api.http` annotations, generates OpenAPI, but has **no server implementation** in the worker (`/v1alpha` routing only wires up `handleReviews`, i.e. the Review API above; `CatalogService`'s `ListFestivals`/`GetFestival`/`ListDrinks` RPCs have no handler) | `proto/cambeerfestival/festival/v1alpha/{catalog_service,my_festival_service,drink_entry,drink_summary,festival,drink,producer}.proto``buf generate``docs/code/api/openapi/openapi.yaml` (Redoc-published by `api-docs.yml`) | Design-time contract for where the API is headed: a typed, resource-oriented catalogue API (AIP-compliant) and a richer `DrinkEntry`/`MyFestivalService` (favourite/rating/note/pour-count sync with soft-delete tombstones and etag concurrency) intended to eventually replace/extend the anonymous Review API. Full workflow and AIP facts: skill `api-contract`. |
278278

279279
The **static feeds** and the **v1alpha Review API** are the two surfaces that
@@ -445,8 +445,8 @@ cat docs/code/api/festival-registry-schema.json
445445
# API surface: is CatalogService actually routed in the worker? (expect: no handler, only handleReviews)
446446
grep -n "v1alpha\|handleReviews\|CatalogService" cloudflare-worker/worker.js
447447

448-
# D1 provisioning state (expect: placeholder database_id until provisioned)
449-
grep -n "database_id" cloudflare-worker/wrangler.toml
448+
# D1 provisioning state (expect: [[d1_databases]] commented out until provisioned)
449+
grep -n "d1_databases" -A5 cloudflare-worker/wrangler.toml
450450

451451
# Proto promotion path / AIP framing
452452
sed -n '1,40p' proto/buf.yaml

.claude/skills/run-and-operate/SKILL.md

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -196,36 +196,56 @@ enrollment; every release after that is fully automated.
196196
> changes near/during the live festival; see skill `change-control`). Do not
197197
> run the commands below on your own judgement.
198198
199-
`cloudflare-worker/wrangler.toml` has a **placeholder** database id:
200-
201-
```toml
202-
[[d1_databases]]
203-
binding = "RATINGS_DB"
204-
database_name = "cbf-myfestival"
205-
database_id = "00000000-0000-0000-0000-000000000000"
206-
migrations_dir = "migrations"
207-
```
199+
`cloudflare-worker/wrangler.toml` has **no D1 binding** — the
200+
`[[d1_databases]]` block is commented out, so the deployed worker runs without
201+
`env.RATINGS_DB` and every `/v1alpha` route answers 503 `STORAGE_UNCONFIGURED`
202+
(`reviews.ts:158`).
203+
204+
> **Why commented out, not a placeholder id.** It used to carry
205+
> `database_id = "00000000-0000-0000-0000-000000000000"`. Cloudflare validates
206+
> bindings when the script is uploaded, so that placeholder made **every**
207+
> `wrangler deploy` fail with error 10181 — taking the CORS proxy down with it,
208+
> even though the proxy has nothing to do with D1. The worker was undeployable
209+
> from 2026-06-13 (PR #426, which added the binding) until 2026-08-10, and the
210+
> live worker silently served pre-#426 code that whole time. Do not restore a
211+
> placeholder id "so the config documents itself" — a binding is either real or
212+
> absent.
208213
209214
Tests and `wrangler dev` use a **simulated local D1** (via
210-
`@cloudflare/vitest-pool-workers`) and ignore this id entirely — the whole
211-
worker test suite (`npm test` in `cloudflare-worker/`) runs green with the
212-
placeholder in place, so a passing `test:worker` run tells you nothing about
213-
whether the real database exists.
214-
215-
To provision the real thing (do this before any manual `wrangler deploy` that
216-
needs to serve real `/v1alpha` review traffic):
215+
`@cloudflare/vitest-pool-workers`) and never check the id — a green
216+
`test:worker` run tells you nothing about whether the real database exists, or
217+
even whether the worker can deploy at all. The binding for tests is declared in
218+
`cloudflare-worker/vitest.config.js` (`miniflare.d1Databases`), deliberately
219+
decoupled from `wrangler.toml` so the test suite is unaffected by the
220+
production binding's presence.
221+
222+
**`wrangler deploy --dry-run` does NOT catch a dangling binding** — verified on
223+
wrangler 4.98.0: with the all-zeroes `database_id` restored it printed
224+
`env.RATINGS_DB (cbf-myfestival)` in the binding table and exited 0. Resource
225+
existence is checked server-side at upload, so CI's `validate-worker` job was
226+
green on the PR that broke deploys, and only the post-merge `deploy-worker` job
227+
on `main` failed. The offline guard is
228+
`cloudflare-worker/test/wrangler-config.test.js` (runs in `test-worker`, needs
229+
no credentials); the only positive proof a binding resolves is a real deploy.
230+
231+
To provision the real thing (needed before `/v1alpha` can serve real review
232+
traffic):
217233

218234
```bash
219235
cd cloudflare-worker
220236

221237
# 1. Create the D1 database in the Cloudflare account
222238
wrangler d1 create cbf-myfestival
223-
# → paste the returned database_id into wrangler.toml's database_id field
239+
# → uncomment the [[d1_databases]] block in wrangler.toml and paste the
240+
# returned id into database_id
224241

225242
# 2. Apply migrations to the REAL (remote) database
226243
wrangler d1 migrations apply cbf-myfestival --remote
227244
# (only one migration exists today: migrations/0001_create_reviews_table.sql —
228245
# single `reviews` table, PK (bucket, festival_id, drink_id, device_id))
246+
247+
# 3. Confirm the binding resolves before pushing
248+
npx wrangler deploy --dry-run # must list env.RATINGS_DB (cbf-myfestival)
229249
```
230250

231251
The `CLOUDFLARE_API_TOKEN` used for this needs **D1:Edit** permission in
@@ -399,8 +419,11 @@ Re-verification commands (run when a fact here feels stale):
399419
sed -n '1,50p' .github/workflows/release-pr.yml
400420
sed -n '1,80p' .github/workflows/release.yml
401421

402-
# D1 still unprovisioned (placeholder id)?
403-
grep -A2 database_id cloudflare-worker/wrangler.toml
422+
# D1 still unprovisioned (expect: the [[d1_databases]] block is commented out)?
423+
grep -n "d1_databases" -A5 cloudflare-worker/wrangler.toml
424+
425+
# Placeholder bindings still absent? (--dry-run does NOT check this; this does)
426+
(cd cloudflare-worker && npx vitest run test/wrangler-config.test.js)
404427

405428
# Deployment topology still matches reality (not the stale doc)?
406429
grep -n "project-name" .github/workflows/ci.yml .github/workflows/release-web.yml

0 commit comments

Comments
 (0)