Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions services/actions/schedule_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
webhook_module "code.gitea.io/gitea/modules/webhook"
Expand Down Expand Up @@ -67,7 +68,7 @@ func startTasks(ctx context.Context) error {
continue
}

if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
if err := CreateScheduleTask(ctx, row); err != nil {
log.Error("CreateScheduleTask: %v", err)
return err
}
Expand Down Expand Up @@ -97,9 +98,12 @@ func startTasks(ctx context.Context) error {
return nil
}

// CreateScheduleTask creates a scheduled task from a cron action schedule.
// CreateScheduleTask creates a scheduled task from a cron action schedule spec.
// It creates an action run based on the schedule, inserts it into the database, and creates commit statuses for each job.
func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) error {
func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleSpec) error {
cron := spec.Schedule
eventPayload := withScheduleInEventPayload(cron.EventPayload, spec.Spec)

// Create a new action run based on the schedule
run := &actions_model.ActionRun{
Title: cron.Title,
Expand All @@ -110,7 +114,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
Ref: cron.Ref,
CommitSHA: cron.CommitSHA,
Event: cron.Event,
EventPayload: cron.EventPayload,
EventPayload: eventPayload,
TriggerEvent: string(webhook_module.HookEventSchedule),
ScheduleID: cron.ID,
Status: actions_model.StatusWaiting,
Expand All @@ -126,3 +130,24 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
// Return nil if no errors occurred
return nil
}

func withScheduleInEventPayload(eventPayload, schedule string) string {
if schedule == "" || eventPayload == "" {
return eventPayload
}

event := map[string]any{}
if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
log.Error("withScheduleInEventPayload: unmarshal: %v", err)
return eventPayload
}

event["schedule"] = schedule
Comment on lines +140 to +145
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withScheduleInEventPayload will panic when eventPayload is the JSON literal "null": json.Unmarshal succeeds but sets the map to nil, and then event["schedule"] assignment panics. This can happen because schedule specs created via DetectAndHandleSchedules marshal a nil payload, resulting in "null" being stored. Also, when eventPayload is empty/whitespace, Unmarshal fails and the schedule is silently not added. Consider treating empty/"null" payloads as an empty object ({}), and after unmarshal defensively re-initialize the map if it is nil before setting the "schedule" field.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review is right, who marked it as resolved ........

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "fix" to this review is just wrong f665654

updatedPayload, err := json.Marshal(event)
if err != nil {
log.Error("withScheduleInEventPayload: marshal: %v", err)
return eventPayload
}

return string(updatedPayload)
}
41 changes: 41 additions & 0 deletions services/actions/schedule_tasks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
"testing"

"code.gitea.io/gitea/modules/json"

"github.com/stretchr/testify/assert"
)

func TestWithScheduleInEventPayload(t *testing.T) {
t.Run("adds schedule to existing payload", func(t *testing.T) {
payload := `{"ref":"refs/heads/main"}`
updated := withScheduleInEventPayload(payload, "*/5 * * * *")

event := map[string]any{}
assert.NoError(t, json.Unmarshal([]byte(updated), &event))
assert.Equal(t, "*/5 * * * *", event["schedule"])
assert.Equal(t, "refs/heads/main", event["ref"])
})

t.Run("keeps empty payload", func(t *testing.T) {
updated := withScheduleInEventPayload("", "37 12 5 1 2")
assert.Empty(t, updated)
})

t.Run("keeps payload when schedule empty", func(t *testing.T) {
payload := `{"ref":"refs/heads/main"}`
updated := withScheduleInEventPayload(payload, "")
assert.Equal(t, payload, updated)
})

t.Run("keeps payload when malformed JSON", func(t *testing.T) {
payload := `not a json object`
updated := withScheduleInEventPayload(payload, "*/5 * * * *")
assert.Equal(t, payload, updated)
})
}