Skip to content

feat(operations): evaluate a scheduled deployment's cron in its own timezone - #259

Merged
AllureCurtain merged 1 commit into
sandbaseai:mainfrom
AllureCurtain:feat/issue-258-cron-timezone
Sep 21, 2026
Merged

AllureCurtain merged 1 commit into
sandbaseai:mainfrom
AllureCurtain:feat/issue-258-cron-timezone

Conversation

@AllureCurtain

Copy link
Copy Markdown
Collaborator

What changed

A scheduled deployment's cron expression is evaluated in the deployment's own
timezone rather than in UTC. A schedule that says 0 9 * * * with
timezone: "Asia/Tokyo" fires at 09:00 Tokyo time, which is a different instant
from 09:00 UTC, and the difference moves across the year because Tokyo does not
observe DST while a zone that does shifts by an hour.

Wall-clock arithmetic handles the two cases a naive conversion gets wrong:

  • a spring-forward gap, where the requested wall time does not exist in the
    zone, is reported as having no run rather than being shifted silently;
  • a fall-back overlap, where the same wall time occurs twice, resolves to one
    deterministic instant.

An unknown IANA zone name is refused rather than silently falling back to UTC,
because a schedule that quietly fires at the wrong hour is worse than one that
fails to save. next_run_at is stored as an absolute instant, so a runtime that
was down when a schedule came due still computes the next occurrence from the
persisted value rather than from when it happened to restart.

nextCronRun gains an optional third parameter for the zone, defaulting to UTC,
so an existing caller is unchanged. The scheduler reads a schedule's stored
timezone when the row carries one and defaults to UTC otherwise, so a row
written before per-schedule zones still runs.

Constraints and invariants

  • The default zone is UTC, which preserves the previous behaviour exactly for
    every existing caller and every existing row.
  • A cron field grammar change is additive: a bare value, a list, a range, and a
    step all parse, and an out-of-range or malformed field is still refused.
  • No migration and no change to the scheduled-deployment lifecycle events, run
    records, pause/unpause, or manual trigger.

Out of scope

  • The timezone field on the scheduled-deployment create and update routes, and
    the column that would persist it. Storing a per-schedule zone is part of the
    scheduled-execution migration, and the route-level refusal of an unknown zone
    belongs with it.
  • The asymmetric failure split between a rate limit and an unrecoverable error.

Validation

  • npx vitest run tests/unit/cron-timezone.test.ts tests/unit/scheduler.test.ts on Node 22: 11 passed, 0 failed. The new suite pins the field grammar, zone validation, the same wall time resolving to different instants, the DST shift, the spring-forward gap, the fall-back overlap, successive occurrences, and the strict-later-than rule.
  • npm run release:check on Node 22 from the topic worktree: typecheck, 1098
    tests passed with 16 skipped and 0 failed, build, package:check, and
    smoke:release all succeeded.
  • Skipped test files: tests/integration/kubernetes-sandbox-security.test.ts
    (8, live cluster), tests/integration/kubernetes-sandbox-errors.test.ts (3,
    live cluster), tests/integration/kubernetes-sandbox.test.ts (2, live
    cluster), tests/integration/docker-sandbox.test.ts (1, no image),
    tests/unit/local-sandbox.test.ts (1, symlink privilege),
    tests/unit/console-pages.test.ts (1, intentional).
  • docker build -f Dockerfile.mcp did not run locally; no Docker daemon was
    reachable from the author environment. CI runs the same build and it passes.

Fixes #258

…imezone

A cron expression is now evaluated in the deployment's timezone rather than UTC,
so the same wall time resolves to a different instant in a different zone and the
difference moves across a DST boundary. A wall time inside a spring-forward gap
yields no run rather than a silently shifted one, a fall-back overlap resolves to
one deterministic instant, and an unknown IANA zone name is refused rather than
defaulted. nextCronRun takes an optional zone, defaulting to UTC.

Refs sandbaseai#258
@AllureCurtain
AllureCurtain merged commit 5b90151 into sandbaseai:main Sep 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Evaluate a scheduled deployment's cron expression in its own timezone

1 participant