feat: add timezone field support to on.schedule cron entries#22018
feat: add timezone field support to on.schedule cron entries#22018
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/98f3a3e3-8cab-4909-a319-7ce98f9b7fa8
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/98f3a3e3-8cab-4909-a319-7ce98f9b7fa8
|
@copilot update main_json_schema with timezone element |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/14c13573-e00c-4823-b284-0fc050658b81
Done in 803971b. Added |
There was a problem hiding this comment.
Pull request overview
Adds compiler + schema support for the optional timezone field on on.schedule entries so timezone-aware schedules can be validated and preserved into the compiled GitHub Actions YAML.
Changes:
- Validate
timezone(if present) is a string during schedule preprocessing and keep it in the schedule item map. - Add unit tests covering valid timezones, missing timezone (backward compatibility), mixed timezones, and invalid (non-string) timezone values.
- Extend the workflow JSON schema to allow an optional
timezonestring on schedule items and add an example.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| pkg/workflow/schedule_preprocessing.go | Adds validation for optional timezone on schedule items during preprocessing. |
| pkg/workflow/schedule_preprocessing_test.go | Adds test coverage for timezone validation/preservation behavior. |
| pkg/parser/schemas/main_workflow_schema.json | Updates schema + examples to document/permit timezone on schedule items. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if tzValue, hasTimezone := itemMap["timezone"]; hasTimezone { | ||
| if _, ok := tzValue.(string); !ok { | ||
| return fmt.Errorf("schedule item %d 'timezone' field must be a string (IANA timezone, e.g. \"America/New_York\")", i) | ||
| } |
There was a problem hiding this comment.
When timezone is provided, the cron field can still contain your extended fuzzy syntax with explicit UTC offsets / timezone abbreviations (e.g. utc+9, PT). parser.ParseSchedule converts those to UTC hours, and then GitHub Actions will apply timezone again when interpreting the cron, resulting in an incorrect trigger time. Consider rejecting schedule strings that include explicit UTC offsets / TZ abbreviations when timezone is set (or define a clear precedence and adjust parsing accordingly).
| } | |
| } | |
| // When a top-level 'timezone' is provided, disallow explicit timezone | |
| // abbreviations or UTC offsets in the 'cron' field itself. Otherwise, | |
| // the parser may convert the schedule to UTC and GitHub Actions will | |
| // apply 'timezone' again, resulting in an incorrect trigger time. | |
| lowerCron := strings.ToLower(cronStr) | |
| for _, token := range strings.Fields(lowerCron) { | |
| if strings.HasPrefix(token, "utc+") || strings.HasPrefix(token, "utc-") { | |
| return fmt.Errorf("schedule item %d 'cron' field must not include explicit timezone or UTC offset when 'timezone' is set", i) | |
| } | |
| switch token { | |
| case "pt", "pst", "pdt", | |
| "et", "est,": // allow for common variants, see next case too | |
| fallthrough | |
| case "est", "edt", | |
| "ct", "cst", "cdt", | |
| "mt", "mst", "mdt": | |
| return fmt.Errorf("schedule item %d 'cron' field must not include explicit timezone or UTC offset when 'timezone' is set", i) | |
| } | |
| } |
| tests := []struct { | ||
| name string | ||
| frontmatter map[string]any | ||
| expectedCron string | ||
| expectedTimezone string | ||
| expectedError bool | ||
| errorSubstring string | ||
| }{ | ||
| { | ||
| name: "schedule with valid IANA timezone", | ||
| frontmatter: map[string]any{ | ||
| "on": map[string]any{ | ||
| "schedule": []any{ | ||
| map[string]any{ | ||
| "cron": "30 5 * * 1-5", | ||
| "timezone": "America/New_York", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| expectedCron: "30 5 * * 1-5", | ||
| expectedTimezone: "America/New_York", | ||
| }, | ||
| { | ||
| name: "schedule with UTC timezone", | ||
| frontmatter: map[string]any{ | ||
| "on": map[string]any{ | ||
| "schedule": []any{ | ||
| map[string]any{ | ||
| "cron": "0 2 * * *", | ||
| "timezone": "UTC", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| expectedCron: "0 2 * * *", | ||
| expectedTimezone: "UTC", | ||
| }, | ||
| { | ||
| name: "schedule without timezone (backward compatible)", | ||
| frontmatter: map[string]any{ | ||
| "on": map[string]any{ | ||
| "schedule": []any{ | ||
| map[string]any{ | ||
| "cron": "0 9 * * 1", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| expectedCron: "0 9 * * 1", | ||
| expectedTimezone: "", | ||
| }, | ||
| { | ||
| name: "multiple schedules with mixed timezone usage", | ||
| frontmatter: map[string]any{ | ||
| "on": map[string]any{ | ||
| "schedule": []any{ | ||
| map[string]any{ | ||
| "cron": "30 5 * * 1-4", | ||
| "timezone": "America/New_York", | ||
| }, | ||
| map[string]any{ | ||
| "cron": "30 17 * * 2,4", | ||
| "timezone": "Europe/London", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| // Second schedule item checked separately in the test body below | ||
| expectedCron: "30 5 * * 1-4", | ||
| expectedTimezone: "America/New_York", | ||
| }, | ||
| { | ||
| name: "timezone field must be a string - non-string rejected", | ||
| frontmatter: map[string]any{ | ||
| "on": map[string]any{ | ||
| "schedule": []any{ | ||
| map[string]any{ | ||
| "cron": "0 2 * * *", | ||
| "timezone": 123, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| expectedError: true, | ||
| errorSubstring: "'timezone' field must be a string", | ||
| }, | ||
| } |
There was a problem hiding this comment.
Add a negative test case that combines timezone with a schedule string containing an explicit UTC offset / timezone abbreviation (e.g. cron: "weekly on friday around 8 am PT" or "daily around 14:00 utc+9") to lock in the expected behavior (error or defined precedence). Without this, it’s easy to regress into double-applying time zone conversions.
| "description": "Cron expression using standard format (e.g., '0 9 * * 1') or fuzzy format (e.g., 'daily', 'daily around 14:00', 'daily between 9:00 and 17:00', 'weekly', 'weekly on monday', 'weekly on friday around 5pm', 'hourly', 'every 2h', 'every 10 minutes'). Fuzzy formats support: daily/weekly schedules with optional time windows, hourly intervals with scattered minutes, interval schedules (minimum 5 minutes), short duration units (m/h/d/w), and UTC timezone offsets (utc+N or utc+HH:MM)." | ||
| }, | ||
| "timezone": { | ||
| "type": "string", | ||
| "description": "Optional IANA timezone string for timezone-aware scheduling (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo', 'UTC'). When set, the cron expression is interpreted in the specified timezone instead of UTC." |
There was a problem hiding this comment.
The schema now documents both embedded UTC offset support in the cron string (via the fuzzy syntax) and the new timezone field. Consider clarifying that timezone should not be used together with embedded utc+/- offsets or TZ abbreviations in the cron value (or specify which takes precedence), since combining them can lead to incorrect scheduling after compilation.
| "description": "Cron expression using standard format (e.g., '0 9 * * 1') or fuzzy format (e.g., 'daily', 'daily around 14:00', 'daily between 9:00 and 17:00', 'weekly', 'weekly on monday', 'weekly on friday around 5pm', 'hourly', 'every 2h', 'every 10 minutes'). Fuzzy formats support: daily/weekly schedules with optional time windows, hourly intervals with scattered minutes, interval schedules (minimum 5 minutes), short duration units (m/h/d/w), and UTC timezone offsets (utc+N or utc+HH:MM)." | |
| }, | |
| "timezone": { | |
| "type": "string", | |
| "description": "Optional IANA timezone string for timezone-aware scheduling (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo', 'UTC'). When set, the cron expression is interpreted in the specified timezone instead of UTC." | |
| "description": "Cron expression using standard format (e.g., '0 9 * * 1') or fuzzy format (e.g., 'daily', 'daily around 14:00', 'daily between 9:00 and 17:00', 'weekly', 'weekly on monday', 'weekly on friday around 5pm', 'hourly', 'every 2h', 'every 10 minutes'). Fuzzy formats support: daily/weekly schedules with optional time windows, hourly intervals with scattered minutes, interval schedules (minimum 5 minutes), short duration units (m/h/d/w), and UTC timezone offsets (utc+N or utc+HH:MM). When using a separate 'timezone' field, avoid including embedded 'utc+/-' offsets or timezone abbreviations in the cron string; if both are present, the 'timezone' field takes precedence for interpreting the schedule." | |
| }, | |
| "timezone": { | |
| "type": "string", | |
| "description": "Optional IANA timezone string for timezone-aware scheduling (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo', 'UTC'). When set, the cron expression is interpreted in the specified timezone instead of UTC. Do not combine this field with embedded 'utc+/-' offsets or timezone abbreviations in the cron value; if such offsets/abbreviations are present, this 'timezone' field takes precedence." |
Add documentation for the new `timezone` field on schedule items, which allows cron expressions to be interpreted in a specific IANA timezone instead of UTC (from #22018). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Adds compiler support for the optional
timezonefield inon.scheduleitems, as documented in the GitHub Actions workflow syntax.Changes
pkg/workflow/schedule_preprocessing.go: In the schedule array processing loop, validate the optionaltimezonefield (must be a string if present) and preserve it in the map so it passes through to the compiled YAML output unchanged.pkg/workflow/schedule_preprocessing_test.go: AddedTestScheduleTimezoneFieldwith cases covering valid IANA timezone, UTC timezone, missing timezone (backward compat), multiple schedules with mixed timezones, and non-string timezone rejection.pkg/parser/schemas/main_workflow_schema.json: Addedtimezoneas an optional string property to the schedule item schema definition, with an IANA timezone description and a usage example.Example
Workflows can now use timezone-aware schedule syntax:
Which compiles to the equivalent GitHub Actions YAML with the
timezonefield preserved.Security Summary
No security vulnerabilities were introduced or discovered. CodeQL analysis found 0 alerts.
⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.