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  }