forked from go-gitea/gitea
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschedule_tasks.go
More file actions
184 lines (162 loc) · 5.74 KB
/
Copy pathschedule_tasks.go
File metadata and controls
184 lines (162 loc) · 5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"errors"
"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
func StartScheduleTasks(ctx context.Context) error {
return startTasks(ctx)
}
// startTasks starts every due spec and returns an error if any of them failed.
func startTasks(ctx context.Context) error {
const pageSize = 50
now := time.Now()
var failed int
var beforeID int64
for {
specs, _, err := actions_model.FindSpecs(ctx, actions_model.FindSpecOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: pageSize},
Next: now.Unix(),
BeforeID: beforeID,
})
if err != nil {
return fmt.Errorf("find specs: %w", err)
}
for _, row := range specs {
// one failing spec must not abort the pass, or a single broken workflow stops every other schedule
if err := startTask(ctx, row, now); err != nil {
failed++
log.Error("start schedule spec %d (repo %d, schedule %d): %v", row.ID, row.RepoID, row.ScheduleID, err)
}
}
if len(specs) < pageSize {
break
}
beforeID = specs[len(specs)-1].ID // keyset, because advanced specs drop out of the "next <= now" filter
}
// surfaces as an admin notice through the cron task, once per occurrence rather than once per pass
if failed > 0 {
return fmt.Errorf("%d schedule(s) could not be started", failed)
}
return nil
}
// startTask advances the spec to its next occurrence before creating the run, so a failing workflow
// retries on its own schedule instead of on every pass, and a failed update cannot duplicate the run.
func startTask(ctx context.Context, row *actions_model.ActionScheduleSpec, now time.Time) error {
schedule, err := row.Parse()
if err != nil {
return fmt.Errorf("parse %q: %w", row.Spec, err)
}
row.Prev = row.Next
row.Next = timeutil.TimeStamp(schedule.Next(now.Add(time.Minute)).Unix())
if err := actions_model.UpdateScheduleSpec(ctx, row, "prev", "next"); err != nil {
return fmt.Errorf("update spec: %w", err)
}
if row.Repo == nil || row.Schedule == nil {
return errors.New("repo or schedule row is gone")
}
if row.Repo.IsArchived {
return nil
}
cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
if err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
return nil
}
return fmt.Errorf("GetUnit: %w", err)
}
if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) {
return nil
}
if err := CreateScheduleTask(ctx, row); err != nil {
return fmt.Errorf("create run for %s workflow %q: %w", row.Repo.FullName(), row.Schedule.WorkflowID, err)
}
return nil
}
// 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, spec *actions_model.ActionScheduleSpec) error {
cron := spec.Schedule
// 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{
Title: cron.Title,
RepoID: cron.RepoID,
OwnerID: cron.OwnerID,
WorkflowID: cron.WorkflowID,
TriggerUserID: cron.TriggerUserID,
Ref: cron.Ref,
CommitSHA: cron.CommitSHA,
Event: cron.Event,
EventPayload: eventPayload,
TriggerEvent: string(webhook_module.HookEventSchedule),
ScheduleID: cron.ID,
Status: actions_model.StatusWaiting,
// schedule runs the repo's own workflow at the recorded commit
WorkflowRepoID: cron.RepoID,
WorkflowCommitSHA: cron.CommitSHA,
}
// FIXME cron.Content might be outdated if the workflow file has been changed.
// Load the latest sha from default branch
// Insert the action run and its associated jobs into the database
if err := PrepareRunAndInsert(ctx, cron.Content, run, nil); err != nil {
return err
}
// Return nil if no errors occurred
return nil
}
func withScheduleInEventPayload(eventPayload, schedule string, fields map[string]any) string {
if schedule == "" {
return eventPayload
}
// eventPayload originates from json.Marshal(input.Payload) in handleSchedules,
// so a nil payload is stored as the literal "null" and pre-existing rows may be
// empty. Both cases start from a fresh map so the schedule field can still be set.
var event map[string]any
if eventPayload != "" {
if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
log.Error("withScheduleInEventPayload: unmarshal: %v", err)
return eventPayload
}
}
if event == nil {
event = map[string]any{}
}
maps.Copy(event, fields)
event["schedule"] = schedule
updatedPayload, err := json.Marshal(event)
if err != nil {
log.Error("withScheduleInEventPayload: marshal: %v", err)
return eventPayload
}
return string(updatedPayload)
}