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  }