@@ -22,91 +22,94 @@ import (
2222 "gitea.dev/modules/timeutil"
2323 webhook_module "gitea.dev/modules/webhook"
2424 "gitea.dev/services/convert"
25+
26+ "xorm.io/builder"
2527)
2628
2729// StartScheduleTasks start the task
2830func StartScheduleTasks (ctx context.Context ) error {
2931 return startTasks (ctx )
3032}
3133
32- // startTasks retrieves specifications in pages, creates a schedule task for each specification,
33- // and updates the specification's next run time and previous run time.
34- // The function returns an error if there's an issue with finding or updating the specifications.
34+ // startTasks starts every due spec and returns an error if any of them failed.
3535func startTasks (ctx context.Context ) error {
36- // Set the page size
37- pageSize := 50
38-
39- // Retrieve specs in pages until all specs have been retrieved
36+ var failed int
4037 now := time .Now ()
41- for page := 1 ; ; page ++ {
42- // Retrieve the specs for the current page
43- specs , _ , err := actions_model .FindSpecs (ctx , actions_model.FindSpecOptions {
44- ListOptions : db.ListOptions {
45- Page : page ,
46- PageSize : pageSize ,
47- },
48- Next : now .Unix (),
49- })
50- if err != nil {
51- return fmt .Errorf ("find specs: %w" , err )
52- }
53-
54- if err := specs .LoadRepos (ctx ); err != nil {
55- return fmt .Errorf ("LoadRepos: %w" , err )
56- }
57-
58- // Loop through each spec and create a schedule task for it
59- for _ , row := range specs {
60- if row .Repo .IsArchived {
61- // Skip if the repo is archived
62- continue
38+ err := db .Iterate (ctx ,
39+ builder .And (builder.Gt {"next" : 0 }, builder.Lte {"next" : now .Unix ()}),
40+ func (ctx context.Context , row * actions_model.ActionScheduleSpec ) error {
41+ // one failing spec must not abort the pass, or a single broken workflow stops every other schedule
42+ if err := startTask (ctx , row , now ); err != nil {
43+ failed ++
44+ log .Error ("start schedule spec %d (repo %d, schedule %d): %v" , row .ID , row .RepoID , row .ScheduleID , err )
6345 }
46+ return nil
47+ })
48+ if err != nil {
49+ return fmt .Errorf ("iterate specs: %w" , err )
50+ }
6451
65- cfg , err := row .Repo .GetUnit (ctx , unit .TypeActions )
66- if err != nil {
67- if repo_model .IsErrUnitTypeNotExist (err ) {
68- // Skip the actions unit of this repo is disabled.
69- continue
70- }
71- return fmt .Errorf ("GetUnit: %w" , err )
72- }
73- if cfg .ActionsConfig ().IsWorkflowDisabled (row .Schedule .WorkflowID ) {
74- continue
75- }
52+ // surfaces as an admin notice through the cron task, once per occurrence rather than once per pass
53+ if failed > 0 {
54+ return fmt .Errorf ("%d schedule(s) could not be started" , failed )
55+ }
56+ return nil
57+ }
7658
77- if err := CreateScheduleTask (ctx , row ); err != nil {
78- log .Error ("CreateScheduleTask: %v" , err )
79- return err
80- }
59+ // startTask advances the spec to its next occurrence before creating the run, so a failing workflow
60+ // retries on its own schedule instead of on every pass, and a failed update cannot duplicate the run.
61+ func startTask (ctx context.Context , row * actions_model.ActionScheduleSpec , now time.Time ) error {
62+ cronSchedule , err := row .Parse ()
63+ if err != nil {
64+ return fmt .Errorf ("parse %q: %w" , row .Spec , err )
65+ }
66+ row .Prev = row .Next
67+ row .Next = timeutil .TimeStamp (cronSchedule .Next (now .Add (time .Minute )).Unix ())
68+ if err := actions_model .UpdateScheduleSpec (ctx , row , "prev" , "next" ); err != nil {
69+ return fmt .Errorf ("update spec: %w" , err )
70+ }
8171
82- // Parse the spec
83- schedule , err := row .Parse ()
84- if err != nil {
85- log .Error ("Parse: %v" , err )
86- return err
87- }
72+ // a spec whose schedule or repo row is gone is skipped, not reported on every occurrence
73+ schedule , exist , err := db .GetByID [actions_model.ActionSchedule ](ctx , row .ScheduleID )
74+ if err != nil {
75+ return fmt .Errorf ("get schedule %d: %w" , row .ScheduleID , err )
76+ } else if ! exist {
77+ return nil
78+ }
79+ repo , exist , err := db .GetByID [repo_model.Repository ](ctx , row .RepoID )
80+ if err != nil {
81+ return fmt .Errorf ("get repo %d: %w" , row .RepoID , err )
82+ } else if ! exist {
83+ return nil
84+ }
85+ row .Schedule , row .Repo = schedule , repo
8886
89- // Update the spec's next run time and previous run time
90- row .Prev = row .Next
91- row .Next = timeutil .TimeStamp (schedule .Next (now .Add (1 * time .Minute )).Unix ())
92- if err := actions_model .UpdateScheduleSpec (ctx , row , "prev" , "next" ); err != nil {
93- log .Error ("UpdateScheduleSpec: %v" , err )
94- return err
95- }
96- }
87+ // only archived repos are skipped; mirrors keep their schedules because a mirror is a normal repo
88+ // for Actions, and nightly builds or scans of the mirrored code are a common reason to run one
89+ if row .Repo .IsArchived {
90+ return nil
91+ }
9792
98- // Stop if all specs have been retrieved
99- if len (specs ) < pageSize {
100- break
93+ cfg , err := row .Repo .GetUnit (ctx , unit .TypeActions )
94+ if err != nil {
95+ if repo_model .IsErrUnitTypeNotExist (err ) {
96+ return nil
10197 }
98+ return fmt .Errorf ("GetUnit: %w" , err )
99+ }
100+ if cfg .ActionsConfig ().IsWorkflowDisabled (row .Schedule .WorkflowID ) {
101+ return nil
102102 }
103103
104+ if err := CreateScheduleTaskBySpec (ctx , row ); err != nil {
105+ return fmt .Errorf ("create run for %s workflow %q: %w" , row .Repo .FullName (), row .Schedule .WorkflowID , err )
106+ }
104107 return nil
105108}
106109
107- // CreateScheduleTask creates a scheduled task from a cron action schedule spec.
110+ // CreateScheduleTaskBySpec creates a scheduled task from a cron action schedule spec.
108111// It creates an action run based on the schedule, inserts it into the database, and creates commit statuses for each job.
109- func CreateScheduleTask (ctx context.Context , spec * actions_model.ActionScheduleSpec ) error {
112+ func CreateScheduleTaskBySpec (ctx context.Context , spec * actions_model.ActionScheduleSpec ) error {
110113 cron := spec .Schedule
111114
112115 // Scheduled runs carry no webhook payload; synthesize what github.event.* expects.
0 commit comments