Skip to content

Runbook: Worker deploy

How to deploy / verify / fix the OTQCheckinAgent Worker.

Standard deploy

cd ~/garmin-warehouse/cloudflare/otq-checkin
npx tsc --noEmit       # type-check first
npx wrangler deploy

Wrangler output should show: - Uploaded otq-checkin (Xs) - Deployed otq-checkin triggers (Ys) - Both crons listed: 0 3 * * * and 30 15 * * * - A new Current Version ID

Smoke test after deploy

# Worker reachable?
curl https://otq-checkin.manoscasey.workers.dev/health
# → "ok"

# DO state intact?
curl https://otq-checkin.manoscasey.workers.dev/stats
# → {"checkins":N,"processed_replies":M,"daily_summaries":P}

# Trigger a morning summary manually:
SECRET=$(echo -n "$TELEGRAM_BOT_TOKEN" | tail -c 12)
curl -X POST "https://otq-checkin.manoscasey.workers.dev/run-morning-summary?secret=$SECRET"
# → {"sent":true,"reason":"message_id=N"} (or {"sent":false,...} if already sent today)

# Live tail:
npx wrangler tail

When deploy fails

wrangler deploy 400 / authentication error

OAuth token expired (happens ~6mo).

npx wrangler logout
npx wrangler login
# Follow OAuth flow in browser, then retry deploy.

TypeScript errors

# What's wrong:
npx tsc --noEmit

# Common: missing types after a package upgrade. Reinstall deps:
npm install

# Common: a new DO method's signature is wrong. Read the error,
# usually a Promise<T> vs T mismatch.

Migration tag bump needed

Adding a NEW DO class (not a new table) requires a migration tag bump in wrangler.jsonc:

"migrations": [
  { "tag": "v1", "new_sqlite_classes": ["OTQCheckinAgent"] },
  { "tag": "v2", "new_sqlite_classes": ["NewClass"] }   // example
]

Adding a new TABLE inside the existing DO class does NOT need a tag bump — the constructor's CREATE TABLE IF NOT EXISTS handles it.

R2 binding missing or wrong bucket name

Symptom: Worker error logs show "WAREHOUSE_DATA is not defined" or similar.

Check wrangler.jsonc:

"r2_buckets": [
  { "binding": "WAREHOUSE_DATA", "bucket_name": "garmin-warehouse-data" }
]

Bucket name must match exactly (Cloudflare is case-sensitive).

Rollback

Cloudflare Workers keeps the last few versions. To roll back:

# List recent versions:
npx wrangler versions list

# Roll back to a specific Version ID:
npx wrangler versions deploy <VERSION_ID>

OR redeploy from a known-good git SHA:

cd ~/garmin-warehouse
git stash    # if you have uncommitted changes
git checkout <good-sha>
cd cloudflare/otq-checkin
npx wrangler deploy
git checkout main
git stash pop

Re-register the Telegram webhook

If the webhook URL changes or webhook gets deregistered:

TG_TOKEN=$(grep TELEGRAM_BOT_TOKEN ~/.zshrc | head -1 | cut -d= -f2- | tr -d '"')
curl -X POST "https://api.telegram.org/bot${TG_TOKEN}/setWebhook" \
  -d "url=https://otq-checkin.manoscasey.workers.dev/webhook"

# Verify:
curl "https://api.telegram.org/bot${TG_TOKEN}/getWebhookInfo"
# Look for url + 0 pending + no last_error

Set / rotate Worker secrets

cd ~/garmin-warehouse/cloudflare/otq-checkin
npx wrangler secret put ANTHROPIC_API_KEY     # prompts for value
npx wrangler secret put INTERVALS_ICU_KEY
npx wrangler secret put TELEGRAM_BOT_TOKEN

# List secret names (values not shown):
npx wrangler secret list

Inspect DO state

The DO has SqlStorage but no direct external query interface — you'd have to add an endpoint. The /stats endpoint exposes counts. For deeper inspection during debugging, add a debug endpoint to index.ts that exposes recent rows:

if (url.pathname === "/debug/recent-checkins" && gateOk) {
  // ... query DO for last 10 rows ...
}

Don't ship debug endpoints permanently.

When the cron didn't fire

# Check Worker observability for cron invocations:
# Cloudflare dashboard → Workers → otq-checkin → Logs → filter "cron"

# Or live tail at fire time:
npx wrangler tail
# Wait for the next scheduled fire (8pm or 8:30am PDT).

If cron is registered but didn't fire: check the wrangler.jsonc crons field is correct (UTC). Confirm via post-deploy output.

If cron fires but doesn't do anything: index.ts scheduled() routes by event.cron string match. If the cron string in wrangler.jsonc differs from what scheduled() matches against, you'll see the "unknown cron pattern" warning in logs.