From dde98cfcd28b1ca81176e67faf688b3cca190773 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 19:00:36 +0000
Subject: [PATCH 1/3] chore: initial progress report for timezone schedule
 support

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
---
 .github/workflows/smoke-codex.lock.yml | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml
index e93417720da..fc04bf871e3 100644
--- a/.github/workflows/smoke-codex.lock.yml
+++ b/.github/workflows/smoke-codex.lock.yml
@@ -450,13 +450,20 @@ jobs:
               {
                 "description": "Add the 'smoked' label to the current pull request (can only be called once)",
                 "inputSchema": {
-                  "additionalProperties": true,
+                  "additionalProperties": false,
                   "properties": {
-                    "payload": {
-                      "description": "JSON-encoded payload to pass to the action",
+                    "labels": {
+                      "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.",
+                      "type": "string"
+                    },
+                    "number": {
+                      "description": "The number of the issue or pull request.",
                       "type": "string"
                     }
                   },
+                  "required": [
+                    "labels"
+                  ],
                   "type": "object"
                 },
                 "name": "add_smoked_label"
@@ -1561,7 +1568,8 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ github.token }}
         with:
-          payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }}
+          labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }}
+          number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }}
       - name: Upload safe output items
         if: always()
         uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0

From ef90894879c16cdb4cb3021cb3498985c916154a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 19:09:37 +0000
Subject: [PATCH 2/3] feat: add timezone field support to on.schedule cron
 entries

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
---
 .github/workflows/smoke-codex.lock.yml      |  16 +-
 pkg/workflow/schedule_preprocessing.go      |   7 +
 pkg/workflow/schedule_preprocessing_test.go | 161 ++++++++++++++++++++
 3 files changed, 172 insertions(+), 12 deletions(-)

diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml
index fc04bf871e3..e93417720da 100644
--- a/.github/workflows/smoke-codex.lock.yml
+++ b/.github/workflows/smoke-codex.lock.yml
@@ -450,20 +450,13 @@ jobs:
               {
                 "description": "Add the 'smoked' label to the current pull request (can only be called once)",
                 "inputSchema": {
-                  "additionalProperties": false,
+                  "additionalProperties": true,
                   "properties": {
-                    "labels": {
-                      "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.",
-                      "type": "string"
-                    },
-                    "number": {
-                      "description": "The number of the issue or pull request.",
+                    "payload": {
+                      "description": "JSON-encoded payload to pass to the action",
                       "type": "string"
                     }
                   },
-                  "required": [
-                    "labels"
-                  ],
                   "type": "object"
                 },
                 "name": "add_smoked_label"
@@ -1568,8 +1561,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ github.token }}
         with:
-          labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }}
-          number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }}
+          payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }}
       - name: Upload safe output items
         if: always()
         uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
diff --git a/pkg/workflow/schedule_preprocessing.go b/pkg/workflow/schedule_preprocessing.go
index 09aff295578..b096f2b0d6a 100644
--- a/pkg/workflow/schedule_preprocessing.go
+++ b/pkg/workflow/schedule_preprocessing.go
@@ -289,6 +289,13 @@ func (c *Compiler) preprocessScheduleFields(frontmatter map[string]any, markdown
 			return fmt.Errorf("schedule item %d 'cron' field must be a string", i)
 		}
 
+		// Validate optional timezone field (IANA timezone string)
+		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)
+			}
+		}
+
 		// Try to parse as human-friendly schedule
 		parsedCron, original, err := c.normalizeScheduleString(cronStr, i)
 		if err != nil {
diff --git a/pkg/workflow/schedule_preprocessing_test.go b/pkg/workflow/schedule_preprocessing_test.go
index ea825c05157..360c268fbce 100644
--- a/pkg/workflow/schedule_preprocessing_test.go
+++ b/pkg/workflow/schedule_preprocessing_test.go
@@ -1742,3 +1742,164 @@ func TestLabelTriggerShorthandPreprocessingErrors(t *testing.T) {
 		}
 	})
 }
+
+// TestScheduleTimezoneField verifies that the optional timezone field in schedule items
+// is validated and preserved in the output, supporting the GitHub Actions on.schedule timezone syntax.
+func TestScheduleTimezoneField(t *testing.T) {
+	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",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			compiler := NewCompiler()
+			compiler.SetWorkflowIdentifier("test-workflow.md")
+
+			err := compiler.preprocessScheduleFields(tt.frontmatter, "", "")
+
+			if tt.expectedError {
+				if err == nil {
+					t.Errorf("expected error containing '%s', got nil", tt.errorSubstring)
+					return
+				}
+				if !strings.Contains(err.Error(), tt.errorSubstring) {
+					t.Errorf("expected error containing '%s', got '%s'", tt.errorSubstring, err.Error())
+				}
+				return
+			}
+
+			if err != nil {
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+
+			onMap := tt.frontmatter["on"].(map[string]any)
+			scheduleArray := onMap["schedule"].([]any)
+			firstSchedule := scheduleArray[0].(map[string]any)
+
+			actualCron, ok := firstSchedule["cron"].(string)
+			if !ok {
+				t.Errorf("expected cron to be string, got %T", firstSchedule["cron"])
+				return
+			}
+			if actualCron != tt.expectedCron {
+				t.Errorf("expected cron '%s', got '%s'", tt.expectedCron, actualCron)
+			}
+
+			if tt.expectedTimezone != "" {
+				actualTimezone, ok := firstSchedule["timezone"].(string)
+				if !ok {
+					t.Errorf("expected timezone field to be preserved as string, got %T", firstSchedule["timezone"])
+					return
+				}
+				if actualTimezone != tt.expectedTimezone {
+					t.Errorf("expected timezone '%s', got '%s'", tt.expectedTimezone, actualTimezone)
+				}
+			} else {
+				if _, hasTimezone := firstSchedule["timezone"]; hasTimezone {
+					t.Errorf("expected no timezone field, but it was present: %v", firstSchedule["timezone"])
+				}
+			}
+
+			// Verify second schedule item for mixed timezone test
+			if tt.name == "multiple schedules with mixed timezone usage" && len(scheduleArray) > 1 {
+				secondSchedule, ok := scheduleArray[1].(map[string]any)
+				if !ok {
+					t.Errorf("expected second schedule to be map, got %T", scheduleArray[1])
+					return
+				}
+				if cron, ok := secondSchedule["cron"].(string); !ok || cron != "30 17 * * 2,4" {
+					t.Errorf("expected second cron '30 17 * * 2,4', got '%v'", secondSchedule["cron"])
+				}
+				if tz, ok := secondSchedule["timezone"].(string); !ok || tz != "Europe/London" {
+					t.Errorf("expected second timezone 'Europe/London', got '%v'", secondSchedule["timezone"])
+				}
+			}
+		})
+	}
+}

From 803971bb5874a0605044c4e19149e3d1647a173e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 20:06:52 +0000
Subject: [PATCH 3/3] feat: add timezone field to main_workflow_schema.json
 schedule items

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
---
 pkg/parser/schemas/main_workflow_schema.json | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 311f2816dd6..07dc039d62d 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -764,6 +764,10 @@
                       "cron": {
                         "type": "string",
                         "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."
                       }
                     },
                     "required": ["cron"],
@@ -1707,6 +1711,15 @@
               ],
               "workflow_dispatch": null
             },
+            {
+              "schedule": [
+                {
+                  "cron": "30 5 * * 1-5",
+                  "timezone": "America/New_York"
+                }
+              ],
+              "workflow_dispatch": null
+            },
             {
               "command": {
                 "name": "mergefest",
