code.gitea.io/gitea@v1.22.3/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 and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 50 Started timeutil.TimeStamp 51 Stopped timeutil.TimeStamp 52 // PreviousDuration is used for recording previous duration 53 PreviousDuration time.Duration 54 Created timeutil.TimeStamp `xorm:"created"` 55 Updated timeutil.TimeStamp `xorm:"updated"` 56 } 57 58 func init() { 59 db.RegisterModel(new(ActionRun)) 60 db.RegisterModel(new(ActionRunIndex)) 61 } 62 63 func (run *ActionRun) HTMLURL() string { 64 if run.Repo == nil { 65 return "" 66 } 67 return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.Index) 68 } 69 70 func (run *ActionRun) Link() string { 71 if run.Repo == nil { 72 return "" 73 } 74 return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) 75 } 76 77 func (run *ActionRun) WorkflowLink() string { 78 if run.Repo == nil { 79 return "" 80 } 81 return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID) 82 } 83 84 // RefLink return the url of run's ref 85 func (run *ActionRun) RefLink() string { 86 refName := git.RefName(run.Ref) 87 if refName.IsPull() { 88 return run.Repo.Link() + "/pulls/" + refName.ShortName() 89 } 90 return git.RefURL(run.Repo.Link(), run.Ref) 91 } 92 93 // PrettyRef return #id for pull ref or ShortName for others 94 func (run *ActionRun) PrettyRef() string { 95 refName := git.RefName(run.Ref) 96 if refName.IsPull() { 97 return "#" + strings.TrimSuffix(strings.TrimPrefix(run.Ref, git.PullPrefix), "/head") 98 } 99 return refName.ShortName() 100 } 101 102 // LoadAttributes load Repo TriggerUser if not loaded 103 func (run *ActionRun) LoadAttributes(ctx context.Context) error { 104 if run == nil { 105 return nil 106 } 107 108 if err := run.LoadRepo(ctx); err != nil { 109 return err 110 } 111 112 if err := run.Repo.LoadAttributes(ctx); err != nil { 113 return err 114 } 115 116 if run.TriggerUser == nil { 117 u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID) 118 if err != nil { 119 return err 120 } 121 run.TriggerUser = u 122 } 123 124 return nil 125 } 126 127 func (run *ActionRun) LoadRepo(ctx context.Context) error { 128 if run == nil || run.Repo != nil { 129 return nil 130 } 131 132 repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) 133 if err != nil { 134 return err 135 } 136 run.Repo = repo 137 return nil 138 } 139 140 func (run *ActionRun) Duration() time.Duration { 141 return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration 142 } 143 144 func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { 145 if run.Event == webhook_module.HookEventPush { 146 var payload api.PushPayload 147 if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { 148 return nil, err 149 } 150 return &payload, nil 151 } 152 return nil, fmt.Errorf("event %s is not a push event", run.Event) 153 } 154 155 func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { 156 if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync { 157 var payload api.PullRequestPayload 158 if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { 159 return nil, err 160 } 161 return &payload, nil 162 } 163 return nil, fmt.Errorf("event %s is not a pull request event", run.Event) 164 } 165 166 func (run *ActionRun) IsSchedule() bool { 167 return run.ScheduleID > 0 168 } 169 170 func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { 171 _, err := db.GetEngine(ctx).ID(repo.ID). 172 SetExpr("num_action_runs", 173 builder.Select("count(*)").From("action_run"). 174 Where(builder.Eq{"repo_id": repo.ID}), 175 ). 176 SetExpr("num_closed_action_runs", 177 builder.Select("count(*)").From("action_run"). 178 Where(builder.Eq{ 179 "repo_id": repo.ID, 180 }.And( 181 builder.In("status", 182 StatusSuccess, 183 StatusFailure, 184 StatusCancelled, 185 StatusSkipped, 186 ), 187 ), 188 ), 189 ). 190 Update(repo) 191 return err 192 } 193 194 // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. 195 // It's useful when a new run is triggered, and all previous runs needn't be continued anymore. 196 func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { 197 // Find all runs in the specified repository, reference, and workflow with non-final status 198 runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ 199 RepoID: repoID, 200 Ref: ref, 201 WorkflowID: workflowID, 202 TriggerEvent: event, 203 Status: []Status{StatusRunning, StatusWaiting, StatusBlocked}, 204 }) 205 if err != nil { 206 return err 207 } 208 209 // If there are no runs found, there's no need to proceed with cancellation, so return nil. 210 if total == 0 { 211 return nil 212 } 213 214 // Iterate over each found run and cancel its associated jobs. 215 for _, run := range runs { 216 // Find all jobs associated with the current run. 217 jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ 218 RunID: run.ID, 219 }) 220 if err != nil { 221 return err 222 } 223 224 // Iterate over each job and attempt to cancel it. 225 for _, job := range jobs { 226 // Skip jobs that are already in a terminal state (completed, cancelled, etc.). 227 status := job.Status 228 if status.IsDone() { 229 continue 230 } 231 232 // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. 233 if job.TaskID == 0 { 234 job.Status = StatusCancelled 235 job.Stopped = timeutil.TimeStampNow() 236 237 // Update the job's status and stopped time in the database. 238 n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") 239 if err != nil { 240 return err 241 } 242 243 // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. 244 if n == 0 { 245 return fmt.Errorf("job has changed, try again") 246 } 247 248 // Continue with the next job. 249 continue 250 } 251 252 // If the job has an associated task, try to stop the task, effectively cancelling the job. 253 if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { 254 return err 255 } 256 } 257 } 258 259 // Return nil to indicate successful cancellation of all running and waiting jobs. 260 return nil 261 } 262 263 // InsertRun inserts a run 264 func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { 265 ctx, committer, err := db.TxContext(ctx) 266 if err != nil { 267 return err 268 } 269 defer committer.Close() 270 271 index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) 272 if err != nil { 273 return err 274 } 275 run.Index = index 276 277 if err := db.Insert(ctx, run); err != nil { 278 return err 279 } 280 281 if run.Repo == nil { 282 repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) 283 if err != nil { 284 return err 285 } 286 run.Repo = repo 287 } 288 289 if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { 290 return err 291 } 292 293 runJobs := make([]*ActionRunJob, 0, len(jobs)) 294 var hasWaiting bool 295 for _, v := range jobs { 296 id, job := v.Job() 297 needs := job.Needs() 298 if err := v.SetJob(id, job.EraseNeeds()); err != nil { 299 return err 300 } 301 payload, _ := v.Marshal() 302 status := StatusWaiting 303 if len(needs) > 0 || run.NeedApproval { 304 status = StatusBlocked 305 } else { 306 hasWaiting = true 307 } 308 job.Name, _ = util.SplitStringAtByteN(job.Name, 255) 309 runJobs = append(runJobs, &ActionRunJob{ 310 RunID: run.ID, 311 RepoID: run.RepoID, 312 OwnerID: run.OwnerID, 313 CommitSHA: run.CommitSHA, 314 IsForkPullRequest: run.IsForkPullRequest, 315 Name: job.Name, 316 WorkflowPayload: payload, 317 JobID: id, 318 Needs: needs, 319 RunsOn: job.RunsOn(), 320 Status: status, 321 }) 322 } 323 if err := db.Insert(ctx, runJobs); err != nil { 324 return err 325 } 326 327 // if there is a job in the waiting status, increase tasks version. 328 if hasWaiting { 329 if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { 330 return err 331 } 332 } 333 334 return committer.Commit() 335 } 336 337 func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { 338 var run ActionRun 339 has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) 340 if err != nil { 341 return nil, err 342 } else if !has { 343 return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist) 344 } 345 346 return &run, nil 347 } 348 349 func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) { 350 run := &ActionRun{ 351 RepoID: repoID, 352 Index: index, 353 } 354 has, err := db.GetEngine(ctx).Get(run) 355 if err != nil { 356 return nil, err 357 } else if !has { 358 return nil, fmt.Errorf("run with index %d %d: %w", repoID, index, util.ErrNotExist) 359 } 360 361 return run, nil 362 } 363 364 func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) { 365 var run ActionRun 366 q := db.GetEngine(ctx).Where("repo_id=?", repoID). 367 And("ref = ?", branch). 368 And("workflow_id = ?", workflowFile) 369 if event != "" { 370 q.And("event = ?", event) 371 } 372 has, err := q.Desc("id").Get(&run) 373 if err != nil { 374 return nil, err 375 } else if !has { 376 return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile) 377 } 378 return &run, nil 379 } 380 381 // UpdateRun updates a run. 382 // It requires the inputted run has Version set. 383 // It will return error if the version is not matched (it means the run has been changed after loaded). 384 func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { 385 sess := db.GetEngine(ctx).ID(run.ID) 386 if len(cols) > 0 { 387 sess.Cols(cols...) 388 } 389 affected, err := sess.Update(run) 390 if err != nil { 391 return err 392 } 393 if affected == 0 { 394 return fmt.Errorf("run has changed") 395 // It's impossible that the run is not found, since Gitea never deletes runs. 396 } 397 398 if run.Status != 0 || slices.Contains(cols, "status") { 399 if run.RepoID == 0 { 400 run, err = GetRunByID(ctx, run.ID) 401 if err != nil { 402 return err 403 } 404 } 405 if run.Repo == nil { 406 repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) 407 if err != nil { 408 return err 409 } 410 run.Repo = repo 411 } 412 if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { 413 return err 414 } 415 } 416 417 return nil 418 } 419 420 type ActionRunIndex db.ResourceIndex