code.gitea.io/gitea@v1.21.7/models/actions/run.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package actions 5 6 import ( 7 "context" 8 "fmt" 9 "slices" 10 "strings" 11 "time" 12 13 "code.gitea.io/gitea/models/db" 14 repo_model "code.gitea.io/gitea/models/repo" 15 user_model "code.gitea.io/gitea/models/user" 16 "code.gitea.io/gitea/modules/git" 17 "code.gitea.io/gitea/modules/json" 18 api "code.gitea.io/gitea/modules/structs" 19 "code.gitea.io/gitea/modules/timeutil" 20 "code.gitea.io/gitea/modules/util" 21 webhook_module "code.gitea.io/gitea/modules/webhook" 22 23 "github.com/nektos/act/pkg/jobparser" 24 "xorm.io/builder" 25 ) 26 27 // ActionRun represents a run of a workflow file 28 type ActionRun struct { 29 ID int64 30 Title string 31 RepoID int64 `xorm:"index unique(repo_index)"` 32 Repo *repo_model.Repository `xorm:"-"` 33 OwnerID int64 `xorm:"index"` 34 WorkflowID string `xorm:"index"` // the name of workflow file 35 Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository 36 TriggerUserID int64 `xorm:"index"` 37 TriggerUser *user_model.User `xorm:"-"` 38 ScheduleID int64 39 Ref string `xorm:"index"` // the commit/tag/… that caused the run 40 CommitSHA string 41 IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. 42 NeedApproval bool // may need approval if it's a fork pull request 43 ApprovedBy int64 `xorm:"index"` // who approved 44 Event webhook_module.HookEventType // the webhook event that causes the workflow to run 45 EventPayload string `xorm:"LONGTEXT"` 46 TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow 47 Status Status `xorm:"index"` 48 Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed 49 Started timeutil.TimeStamp 50 Stopped timeutil.TimeStamp 51 Created timeutil.TimeStamp `xorm:"created"` 52 Updated timeutil.TimeStamp `xorm:"updated"` 53 } 54 55 func init() { 56 db.RegisterModel(new(ActionRun)) 57 db.RegisterModel(new(ActionRunIndex)) 58 } 59 60 func (run *ActionRun) HTMLURL() string { 61 if run.Repo == nil { 62 return "" 63 } 64 return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.Index) 65 } 66 67 func (run *ActionRun) Link() string { 68 if run.Repo == nil { 69 return "" 70 } 71 return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) 72 } 73 74 // RefLink return the url of run's ref 75 func (run *ActionRun) RefLink() string { 76 refName := git.RefName(run.Ref) 77 if refName.IsPull() { 78 return run.Repo.Link() + "/pulls/" + refName.ShortName() 79 } 80 return git.RefURL(run.Repo.Link(), run.Ref) 81 } 82 83 // PrettyRef return #id for pull ref or ShortName for others 84 func (run *ActionRun) PrettyRef() string { 85 refName := git.RefName(run.Ref) 86 if refName.IsPull() { 87 return "#" + strings.TrimSuffix(strings.TrimPrefix(run.Ref, git.PullPrefix), "/head") 88 } 89 return refName.ShortName() 90 } 91 92 // LoadAttributes load Repo TriggerUser if not loaded 93 func (run *ActionRun) LoadAttributes(ctx context.Context) error { 94 if run == nil { 95 return nil 96 } 97 98 if run.Repo == nil { 99 repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) 100 if err != nil { 101 return err 102 } 103 run.Repo = repo 104 } 105 if err := run.Repo.LoadAttributes(ctx); err != nil { 106 return err 107 } 108 109 if run.TriggerUser == nil { 110 u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID) 111 if err != nil { 112 return err 113 } 114 run.TriggerUser = u 115 } 116 117 return nil 118 } 119 120 func (run *ActionRun) Duration() time.Duration { 121 return calculateDuration(run.Started, run.Stopped, run.Status) 122 } 123 124 func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { 125 if run.Event == webhook_module.HookEventPush { 126 var payload api.PushPayload 127 if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { 128 return nil, err 129 } 130 return &payload, nil 131 } 132 return nil, fmt.Errorf("event %s is not a push event", run.Event) 133 } 134 135 func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { 136 if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync { 137 var payload api.PullRequestPayload 138 if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { 139 return nil, err 140 } 141 return &payload, nil 142 } 143 return nil, fmt.Errorf("event %s is not a pull request event", run.Event) 144 } 145 146 func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { 147 _, err := db.GetEngine(ctx).ID(repo.ID). 148 SetExpr("num_action_runs", 149 builder.Select("count(*)").From("action_run"). 150 Where(builder.Eq{"repo_id": repo.ID}), 151 ). 152 SetExpr("num_closed_action_runs", 153 builder.Select("count(*)").From("action_run"). 154 Where(builder.Eq{ 155 "repo_id": repo.ID, 156 }.And( 157 builder.In("status", 158 StatusSuccess, 159 StatusFailure, 160 StatusCancelled, 161 StatusSkipped, 162 ), 163 ), 164 ), 165 ). 166 Update(repo) 167 return err 168 } 169 170 // CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. 171 func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { 172 // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. 173 runs, total, err := FindRuns(ctx, FindRunOptions{ 174 RepoID: repoID, 175 Ref: ref, 176 WorkflowID: workflowID, 177 TriggerEvent: event, 178 Status: []Status{StatusRunning, StatusWaiting}, 179 }) 180 if err != nil { 181 return err 182 } 183 184 // If there are no runs found, there's no need to proceed with cancellation, so return nil. 185 if total == 0 { 186 return nil 187 } 188 189 // Iterate over each found run and cancel its associated jobs. 190 for _, run := range runs { 191 // Find all jobs associated with the current run. 192 jobs, _, err := FindRunJobs(ctx, FindRunJobOptions{ 193 RunID: run.ID, 194 }) 195 if err != nil { 196 return err 197 } 198 199 // Iterate over each job and attempt to cancel it. 200 for _, job := range jobs { 201 // Skip jobs that are already in a terminal state (completed, cancelled, etc.). 202 status := job.Status 203 if status.IsDone() { 204 continue 205 } 206 207 // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. 208 if job.TaskID == 0 { 209 job.Status = StatusCancelled 210 job.Stopped = timeutil.TimeStampNow() 211 212 // Update the job's status and stopped time in the database. 213 n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") 214 if err != nil { 215 return err 216 } 217 218 // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. 219 if n == 0 { 220 return fmt.Errorf("job has changed, try again") 221 } 222 223 // Continue with the next job. 224 continue 225 } 226 227 // If the job has an associated task, try to stop the task, effectively cancelling the job. 228 if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { 229 return err 230 } 231 } 232 } 233 234 // Return nil to indicate successful cancellation of all running and waiting jobs. 235 return nil 236 } 237 238 // InsertRun inserts a run 239 func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { 240 ctx, commiter, err := db.TxContext(ctx) 241 if err != nil { 242 return err 243 } 244 defer commiter.Close() 245 246 index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) 247 if err != nil { 248 return err 249 } 250 run.Index = index 251 252 if err := db.Insert(ctx, run); err != nil { 253 return err 254 } 255 256 if run.Repo == nil { 257 repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) 258 if err != nil { 259 return err 260 } 261 run.Repo = repo 262 } 263 264 if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { 265 return err 266 } 267 268 runJobs := make([]*ActionRunJob, 0, len(jobs)) 269 var hasWaiting bool 270 for _, v := range jobs { 271 id, job := v.Job() 272 needs := job.Needs() 273 if err := v.SetJob(id, job.EraseNeeds()); err != nil { 274 return err 275 } 276 payload, _ := v.Marshal() 277 status := StatusWaiting 278 if len(needs) > 0 || run.NeedApproval { 279 status = StatusBlocked 280 } else { 281 hasWaiting = true 282 } 283 job.Name, _ = util.SplitStringAtByteN(job.Name, 255) 284 runJobs = append(runJobs, &ActionRunJob{ 285 RunID: run.ID, 286 RepoID: run.RepoID, 287 OwnerID: run.OwnerID, 288 CommitSHA: run.CommitSHA, 289 IsForkPullRequest: run.IsForkPullRequest, 290 Name: job.Name, 291 WorkflowPayload: payload, 292 JobID: id, 293 Needs: needs, 294 RunsOn: job.RunsOn(), 295 Status: status, 296 }) 297 } 298 if err := db.Insert(ctx, runJobs); err != nil { 299 return err 300 } 301 302 // if there is a job in the waiting status, increase tasks version. 303 if hasWaiting { 304 if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { 305 return err 306 } 307 } 308 309 return commiter.Commit() 310 } 311 312 func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { 313 var run ActionRun 314 has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) 315 if err != nil { 316 return nil, err 317 } else if !has { 318 return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist) 319 } 320 321 return &run, nil 322 } 323 324 func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) { 325 run := &ActionRun{ 326 RepoID: repoID, 327 Index: index, 328 } 329 has, err := db.GetEngine(ctx).Get(run) 330 if err != nil { 331 return nil, err 332 } else if !has { 333 return nil, fmt.Errorf("run with index %d %d: %w", repoID, index, util.ErrNotExist) 334 } 335 336 return run, nil 337 } 338 339 // UpdateRun updates a run. 340 // It requires the inputted run has Version set. 341 // It will return error if the version is not matched (it means the run has been changed after loaded). 342 func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { 343 sess := db.GetEngine(ctx).ID(run.ID) 344 if len(cols) > 0 { 345 sess.Cols(cols...) 346 } 347 affected, err := sess.Update(run) 348 if err != nil { 349 return err 350 } 351 if affected == 0 { 352 return fmt.Errorf("run has changed") 353 // It's impossible that the run is not found, since Gitea never deletes runs. 354 } 355 356 if run.Status != 0 || slices.Contains(cols, "status") { 357 if run.RepoID == 0 { 358 run, err = GetRunByID(ctx, run.ID) 359 if err != nil { 360 return err 361 } 362 } 363 if run.Repo == nil { 364 repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) 365 if err != nil { 366 return err 367 } 368 run.Repo = repo 369 } 370 if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { 371 return err 372 } 373 } 374 375 return nil 376 } 377 378 type ActionRunIndex db.ResourceIndex