code.gitea.io/gitea@v1.21.7/services/actions/commit_status.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package actions 5 6 import ( 7 "context" 8 "fmt" 9 "path" 10 11 actions_model "code.gitea.io/gitea/models/actions" 12 "code.gitea.io/gitea/models/db" 13 git_model "code.gitea.io/gitea/models/git" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/log" 16 api "code.gitea.io/gitea/modules/structs" 17 webhook_module "code.gitea.io/gitea/modules/webhook" 18 19 "github.com/nektos/act/pkg/jobparser" 20 ) 21 22 // CreateCommitStatus creates a commit status for the given job. 23 // It won't return an error failed, but will log it, because it's not critical. 24 func CreateCommitStatus(ctx context.Context, jobs ...*actions_model.ActionRunJob) { 25 for _, job := range jobs { 26 if err := createCommitStatus(ctx, job); err != nil { 27 log.Error("Failed to create commit status for job %d: %v", job.ID, err) 28 } 29 } 30 } 31 32 func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) error { 33 if err := job.LoadAttributes(ctx); err != nil { 34 return fmt.Errorf("load run: %w", err) 35 } 36 37 run := job.Run 38 39 var ( 40 sha string 41 event string 42 ) 43 switch run.Event { 44 case webhook_module.HookEventPush: 45 event = "push" 46 payload, err := run.GetPushEventPayload() 47 if err != nil { 48 return fmt.Errorf("GetPushEventPayload: %w", err) 49 } 50 if payload.HeadCommit == nil { 51 return fmt.Errorf("head commit is missing in event payload") 52 } 53 sha = payload.HeadCommit.ID 54 case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync: 55 event = "pull_request" 56 payload, err := run.GetPullRequestEventPayload() 57 if err != nil { 58 return fmt.Errorf("GetPullRequestEventPayload: %w", err) 59 } 60 if payload.PullRequest == nil { 61 return fmt.Errorf("pull request is missing in event payload") 62 } else if payload.PullRequest.Head == nil { 63 return fmt.Errorf("head of pull request is missing in event payload") 64 } 65 sha = payload.PullRequest.Head.Sha 66 default: 67 return nil 68 } 69 70 repo := run.Repo 71 // TODO: store workflow name as a field in ActionRun to avoid parsing 72 runName := path.Base(run.WorkflowID) 73 if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 { 74 runName = wfs[0].Name 75 } 76 ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event) 77 state := toCommitStatus(job.Status) 78 if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}); err == nil { 79 for _, v := range statuses { 80 if v.Context == ctxname { 81 if v.State == state { 82 // no need to update 83 return nil 84 } 85 break 86 } 87 } 88 } else { 89 return fmt.Errorf("GetLatestCommitStatus: %w", err) 90 } 91 92 description := "" 93 switch job.Status { 94 // TODO: if we want support description in different languages, we need to support i18n placeholders in it 95 case actions_model.StatusSuccess: 96 description = fmt.Sprintf("Successful in %s", job.Duration()) 97 case actions_model.StatusFailure: 98 description = fmt.Sprintf("Failing after %s", job.Duration()) 99 case actions_model.StatusCancelled: 100 description = "Has been cancelled" 101 case actions_model.StatusSkipped: 102 description = "Has been skipped" 103 case actions_model.StatusRunning: 104 description = "Has started running" 105 case actions_model.StatusWaiting: 106 description = "Waiting to run" 107 case actions_model.StatusBlocked: 108 description = "Blocked by required conditions" 109 } 110 111 index, err := getIndexOfJob(ctx, job) 112 if err != nil { 113 return fmt.Errorf("getIndexOfJob: %w", err) 114 } 115 116 creator := user_model.NewActionsUser() 117 if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ 118 Repo: repo, 119 SHA: sha, 120 Creator: creator, 121 CommitStatus: &git_model.CommitStatus{ 122 SHA: sha, 123 TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index), 124 Description: description, 125 Context: ctxname, 126 CreatorID: creator.ID, 127 State: state, 128 }, 129 }); err != nil { 130 return fmt.Errorf("NewCommitStatus: %w", err) 131 } 132 133 return nil 134 } 135 136 func toCommitStatus(status actions_model.Status) api.CommitStatusState { 137 switch status { 138 case actions_model.StatusSuccess, actions_model.StatusSkipped: 139 return api.CommitStatusSuccess 140 case actions_model.StatusFailure, actions_model.StatusCancelled: 141 return api.CommitStatusFailure 142 case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning: 143 return api.CommitStatusPending 144 default: 145 return api.CommitStatusError 146 } 147 } 148 149 func getIndexOfJob(ctx context.Context, job *actions_model.ActionRunJob) (int, error) { 150 // TODO: store job index as a field in ActionRunJob to avoid this 151 jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID) 152 if err != nil { 153 return 0, err 154 } 155 for i, v := range jobs { 156 if v.ID == job.ID { 157 return i, nil 158 } 159 } 160 return 0, nil 161 }