#13 Cloudflare Cron Trigger: Friday 17:00 America/Chicago, DST-correct #38

Merged
JMR-dev merged 1 commits from ticket-13-cron-trigger into main 2026-07-02 20:54:53 +00:00
JMR-dev commented 2026-07-02 20:50:43 +00:00 (Migrated from github.com)

What & why

Adds the weekly Cron Trigger from #13: Friday 17:00 America/Chicago (Central), DST-correct. On the correct fire it lists all pending reports via #10's Manager.ListPending and hands their ids to the publish step.

Closes #13

The core problem: Cloudflare cron is UTC-only

17:00 Central is a different UTC hour depending on DST — 22:00 UTC during CDT (summer, UTC-5) and 23:00 UTC during CST (winter, UTC-6) — so no single UTC cron expresses it.

Design: two UTC crons + a gate. wrangler.jsonc registers both candidate Friday UTC hours:

"triggers": { "crons": ["0 22 * * 5", "0 23 * * 5"] }

and the scheduled handler gates each fire: only the fire that is actually 17:00 in America/Chicago does the work. Exactly one of the two matches on any given Friday, so publishing runs exactly once per week, correct across the DST boundary, with zero manual maintenance. The sibling fire is a logged no-op.

TinyGo/Wasm tzdata: hand-rolled DST, no tz database

TinyGo/Wasm may not embed the IANA tz database, so time.LoadLocation("America/Chicago") could fail at runtime. The gate therefore does not call LoadLocation. Instead internal/schedule computes the US Central rule from first principles behind a pure function:

func IsFriday1700Central(t time.Time) bool

CDT runs from the 2nd Sunday of March 02:00 to the 1st Sunday of November 02:00 (else CST), converted to absolute UTC boundaries (08:00 UTC spring-forward, 07:00 UTC fall-back). Being a pure func(time.Time) bool with no external data, it is exhaustively host-testable without TinyGo. The Wasm build embeds no tzdata; time/tzdata is imported only in the test file to cross-check the hand-rolled rule against the real zone.

Seam for #14 (publish)

Publishing is not implemented here. The handler calls a schedule.Publisher interface; the default schedule.LogPublisher logs the batch. #14 swaps in the real GitHub-issue publisher by replacing schedule.LogPublisher{} in the wasm adapter — no other change needed.

Files

  • internal/schedule/ — pure DST gate + Run orchestrator + Publisher/PendingLister seams + LogPublisher default. Build-tag-free, host-tested.
  • worker/scheduled_wasm.go — js && wasm-only adapter that registers the scheduled task via init() + cron.ScheduleTaskNonBlock and wires the R2-backed lifecycle.Manager to schedule.Run.
  • wrangler.jsonc — added triggers.crons only.

worker/main.go is untouched (scheduled task registers from a separate init()), to avoid conflicts with the parallel worker-wiring ticket (#11).

Tests / evidence

go vet ./... clean; go test ./... and GOOS=js GOARCH=wasm go build ./... both pass.

Timezone/gate coverage in internal/schedule/schedule_test.go:

  • A Friday in July (CDT) → 22:00 UTC true / 23:00 UTC false; a Friday in January (CST) → 23:00 UTC true / 22:00 UTC false.
  • The Fridays bracketing both DST transitions (spring-forward and fall-back).
  • Wrong hours, wrong minutes, non-Fridays (Thu/Sat at the exact Central hour).
  • TestGateFiresExactlyOncePerFriday: for every Friday 2024–2030, exactly one of the two cron hours passes — never zero, never both.
  • TestGateMatchesIANAAcrossDSTBoundary: 20-year, 30-minute sweep cross-checked against the real America/Chicago zone — zero mismatches (verifies DST correctness without waiting for a real transition).
  • Run with a mock lifecycle + mock publisher: publishes exactly the pending ids on a gate-open fire; touches neither on a gate-closed fire; propagates lister/publisher errors.

Note: TinyGo is not run here (not installed locally per the task); the end-to-end TinyGo/Wasm Worker build is exercised by CI (pnpm run build). The standard-compiler GOOS=js GOARCH=wasm build of the wasm adapter passes locally as a proxy.

🤖 Generated with Claude Code

## What & why Adds the weekly Cron Trigger from #13: **Friday 17:00 America/Chicago (Central), DST-correct**. On the correct fire it lists all `pending` reports via #10's `Manager.ListPending` and hands their ids to the publish step. `Closes #13` ## The core problem: Cloudflare cron is UTC-only 17:00 Central is a **different UTC hour depending on DST** — 22:00 UTC during CDT (summer, UTC-5) and 23:00 UTC during CST (winter, UTC-6) — so no single UTC cron expresses it. **Design: two UTC crons + a gate.** `wrangler.jsonc` registers *both* candidate Friday UTC hours: ```jsonc "triggers": { "crons": ["0 22 * * 5", "0 23 * * 5"] } ``` and the scheduled handler **gates** each fire: only the fire that is *actually* 17:00 in America/Chicago does the work. Exactly one of the two matches on any given Friday, so publishing runs **exactly once per week**, correct across the DST boundary, with zero manual maintenance. The sibling fire is a logged no-op. ## TinyGo/Wasm tzdata: hand-rolled DST, no tz database TinyGo/Wasm may not embed the IANA tz database, so `time.LoadLocation("America/Chicago")` could fail at runtime. The gate therefore **does not call `LoadLocation`**. Instead `internal/schedule` computes the US Central rule from first principles behind a pure function: ```go func IsFriday1700Central(t time.Time) bool ``` CDT runs from the 2nd Sunday of March 02:00 to the 1st Sunday of November 02:00 (else CST), converted to absolute UTC boundaries (08:00 UTC spring-forward, 07:00 UTC fall-back). Being a pure `func(time.Time) bool` with no external data, it is **exhaustively host-testable without TinyGo**. The Wasm build embeds **no** tzdata; `time/tzdata` is imported **only in the test file** to cross-check the hand-rolled rule against the real zone. ## Seam for #14 (publish) Publishing is **not** implemented here. The handler calls a `schedule.Publisher` interface; the default `schedule.LogPublisher` logs the batch. #14 swaps in the real GitHub-issue publisher by replacing `schedule.LogPublisher{}` in the wasm adapter — no other change needed. ## Files - `internal/schedule/` — pure DST gate + `Run` orchestrator + `Publisher`/`PendingLister` seams + `LogPublisher` default. Build-tag-free, host-tested. - `worker/scheduled_wasm.go` — `js && wasm`-only adapter that registers the scheduled task via `init()` + `cron.ScheduleTaskNonBlock` and wires the R2-backed `lifecycle.Manager` to `schedule.Run`. - `wrangler.jsonc` — added `triggers.crons` only. **`worker/main.go` is untouched** (scheduled task registers from a separate `init()`), to avoid conflicts with the parallel worker-wiring ticket (#11). ## Tests / evidence `go vet ./...` clean; `go test ./...` and `GOOS=js GOARCH=wasm go build ./...` both pass. Timezone/gate coverage in `internal/schedule/schedule_test.go`: - A Friday in **July (CDT)** → 22:00 UTC true / 23:00 UTC false; a Friday in **January (CST)** → 23:00 UTC true / 22:00 UTC false. - The Fridays **bracketing both DST transitions** (spring-forward and fall-back). - Wrong hours, wrong minutes, non-Fridays (Thu/Sat at the exact Central hour). - `TestGateFiresExactlyOncePerFriday`: for every Friday 2024–2030, **exactly one** of the two cron hours passes — never zero, never both. - `TestGateMatchesIANAAcrossDSTBoundary`: 20-year, 30-minute sweep cross-checked against the real `America/Chicago` zone — **zero** mismatches (verifies DST correctness without waiting for a real transition). - `Run` with a mock lifecycle + mock publisher: publishes exactly the pending ids on a gate-open fire; touches neither on a gate-closed fire; propagates lister/publisher errors. > Note: TinyGo is not run here (not installed locally per the task); the end-to-end TinyGo/Wasm Worker build is exercised by CI (`pnpm run build`). The standard-compiler `GOOS=js GOARCH=wasm` build of the wasm adapter passes locally as a proxy. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.