From 0e4d93537aa7bc5186cf66585ab43cc2789e7dec Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 14 Jul 2026 18:30:16 +0200 Subject: [PATCH] fix(actions): populate `github.event` for scheduled runs (#38446) Scheduled runs stored a `null` event payload, so `github.event.repository`/`sender`/`organization` were empty in scheduled workflows. Every other event carries them, and so does GitHub Actions. `CreateScheduleTask` now synthesizes them. --- services/actions/schedule_tasks.go | 23 ++++++++++++++++++++-- services/actions/schedule_tasks_test.go | 26 ++++++++++++++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index d67bf8dd29d..0decf7b9689 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -6,16 +6,22 @@ package actions import ( "context" "fmt" + "maps" "time" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" + "gitea.dev/models/organization" + perm_model "gitea.dev/models/perm" + access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" + user_model "gitea.dev/models/user" "gitea.dev/modules/json" "gitea.dev/modules/log" "gitea.dev/modules/timeutil" webhook_module "gitea.dev/modules/webhook" + "gitea.dev/services/convert" ) // StartScheduleTasks start the task @@ -102,7 +108,19 @@ func startTasks(ctx context.Context) error { // 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, spec *actions_model.ActionScheduleSpec) error { cron := spec.Schedule - eventPayload := withScheduleInEventPayload(cron.EventPayload, spec.Spec) + + // Scheduled runs carry no webhook payload; synthesize what github.event.* expects. + if err := spec.Repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) + } + fields := map[string]any{ + "repository": convert.ToRepo(ctx, spec.Repo, access_model.Permission{AccessMode: perm_model.AccessModeRead}), + "sender": convert.ToUser(ctx, user_model.NewActionsUser(), nil), + } + if spec.Repo.Owner.IsOrganization() { + fields["organization"] = convert.ToOrganization(ctx, organization.OrgFromUser(spec.Repo.Owner)) + } + eventPayload := withScheduleInEventPayload(cron.EventPayload, spec.Spec, fields) // Create a new action run based on the schedule run := &actions_model.ActionRun{ @@ -134,7 +152,7 @@ func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleS return nil } -func withScheduleInEventPayload(eventPayload, schedule string) string { +func withScheduleInEventPayload(eventPayload, schedule string, fields map[string]any) string { if schedule == "" { return eventPayload } @@ -153,6 +171,7 @@ func withScheduleInEventPayload(eventPayload, schedule string) string { event = map[string]any{} } + maps.Copy(event, fields) event["schedule"] = schedule updatedPayload, err := json.Marshal(event) if err != nil { diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go index ec68eb54fb6..8514625486c 100644 --- a/services/actions/schedule_tasks_test.go +++ b/services/actions/schedule_tasks_test.go @@ -7,6 +7,7 @@ import ( "testing" "gitea.dev/modules/json" + api "gitea.dev/modules/structs" "github.com/stretchr/testify/assert" ) @@ -14,7 +15,7 @@ import ( 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 * * * *") + updated := withScheduleInEventPayload(payload, "*/5 * * * *", nil) event := map[string]any{} assert.NoError(t, json.Unmarshal([]byte(updated), &event)) @@ -23,7 +24,7 @@ func TestWithScheduleInEventPayload(t *testing.T) { }) t.Run("adds schedule to null payload", func(t *testing.T) { - updated := withScheduleInEventPayload("null", "37 12 5 1 2") + updated := withScheduleInEventPayload("null", "37 12 5 1 2", nil) event := map[string]any{} assert.NoError(t, json.Unmarshal([]byte(updated), &event)) @@ -31,22 +32,37 @@ func TestWithScheduleInEventPayload(t *testing.T) { }) t.Run("adds schedule to empty payload", func(t *testing.T) { - updated := withScheduleInEventPayload("", "37 12 5 1 2") + updated := withScheduleInEventPayload("", "37 12 5 1 2", nil) event := map[string]any{} assert.NoError(t, json.Unmarshal([]byte(updated), &event)) assert.Equal(t, "37 12 5 1 2", event["schedule"]) }) + t.Run("adds schedule with repository, sender, organization", func(t *testing.T) { + updated := withScheduleInEventPayload("null", "@weekly", map[string]any{ + "repository": &api.Repository{Name: "test-repo"}, + "sender": &api.User{UserName: "test-user"}, + "organization": &api.Organization{Name: "test-org"}, + }) + + event := map[string]any{} + assert.NoError(t, json.Unmarshal([]byte(updated), &event)) + assert.Equal(t, "@weekly", event["schedule"]) + assert.Equal(t, "test-repo", event["repository"].(map[string]any)["name"]) + assert.Equal(t, "test-user", event["sender"].(map[string]any)["login"]) + assert.Equal(t, "test-org", event["organization"].(map[string]any)["name"]) + }) + t.Run("keeps payload when schedule empty", func(t *testing.T) { payload := `{"ref":"refs/heads/main"}` - updated := withScheduleInEventPayload(payload, "") + updated := withScheduleInEventPayload(payload, "", nil) 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 * * * *") + updated := withScheduleInEventPayload(payload, "*/5 * * * *", nil) assert.Equal(t, payload, updated) }) }