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