feat(operations): evaluate a scheduled deployment's cron in its own timezone - #259
Merged
AllureCurtain merged 1 commit intoSep 21, 2026
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 * * *withtimezone: "Asia/Tokyo"fires at 09:00 Tokyo time, which is a different instantfrom 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:
zone, is reported as having no run rather than being shifted silently;
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_atis stored as an absolute instant, so a runtime thatwas down when a schedule came due still computes the next occurrence from the
persisted value rather than from when it happened to restart.
nextCronRungains an optional third parameter for the zone, defaulting toUTC,so an existing caller is unchanged. The scheduler reads a schedule's stored
timezonewhen the row carries one and defaults toUTCotherwise, so a rowwritten before per-schedule zones still runs.
Constraints and invariants
UTC, which preserves the previous behaviour exactly forevery existing caller and every existing row.
step all parse, and an out-of-range or malformed field is still refused.
records, pause/unpause, or manual trigger.
Out of scope
timezonefield on the scheduled-deployment create and update routes, andthe 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.
Validation
npx vitest run tests/unit/cron-timezone.test.ts tests/unit/scheduler.test.tson 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:checkon Node 22 from the topic worktree: typecheck, 1098tests passed with 16 skipped and 0 failed, build,
package:check, andsmoke:releaseall succeeded.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, livecluster),
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.mcpdid not run locally; no Docker daemon wasreachable from the author environment. CI runs the same build and it passes.
Fixes #258