mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-21 22:18:23 +01:00
Workflows triggered by pull_request_review events (approved, rejected, comment) complete successfully but never create a commit status on the PR. This makes them invisible in the merge checks UI, breaking any CI gate that re-evaluates on review submission. The commit status handler's switch statement was missing the three review event types, so they fell through to the default case which returned empty strings. Additionally, review events use PullRequestPayload but IsPullRequest() returns false for them (Event() returns "pull_request_approved" etc. instead of "pull_request"), so GetPullRequestEventPayload() refuses to parse their payload. Signed-off-by: Jörg Thalheim <joerg@thalheim.io> Co-authored-by: silverwind <me@silverwind.io>
231 lines
7.5 KiB
Go
231 lines
7.5 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
actions_model "code.gitea.io/gitea/models/actions"
|
|
"code.gitea.io/gitea/models/db"
|
|
git_model "code.gitea.io/gitea/models/git"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
actions_module "code.gitea.io/gitea/modules/actions"
|
|
"code.gitea.io/gitea/modules/commitstatus"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/util"
|
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
|
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
|
|
|
"github.com/nektos/act/pkg/jobparser"
|
|
)
|
|
|
|
// CreateCommitStatusForRunJobs creates a commit status for the given job if it has a supported event and related commit.
|
|
// It won't return an error failed, but will log it, because it's not critical.
|
|
func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.ActionRun, jobs ...*actions_model.ActionRunJob) {
|
|
// don't create commit status for cron job
|
|
if run.ScheduleID != 0 {
|
|
return
|
|
}
|
|
|
|
event, commitID, err := getCommitStatusEventNameAndCommitID(run)
|
|
if err != nil {
|
|
log.Error("GetCommitStatusEventNameAndSHA: %v", err)
|
|
}
|
|
if event == "" || commitID == "" {
|
|
return // unsupported event, or no commit id, or error occurs, do nothing
|
|
}
|
|
|
|
if err = run.LoadAttributes(ctx); err != nil {
|
|
log.Error("run.LoadAttributes: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, job := range jobs {
|
|
if err = createCommitStatus(ctx, run.Repo, event, commitID, run, job); err != nil {
|
|
log.Error("Failed to create commit status for job %d: %v", job.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetRunsFromCommitStatuses(ctx context.Context, statuses []*git_model.CommitStatus) ([]*actions_model.ActionRun, error) {
|
|
runMap := make(map[int64]*actions_model.ActionRun)
|
|
for _, status := range statuses {
|
|
runIndex, _, ok := status.ParseGiteaActionsTargetURL(ctx)
|
|
if !ok {
|
|
continue
|
|
}
|
|
_, ok = runMap[runIndex]
|
|
if !ok {
|
|
run, err := actions_model.GetRunByIndex(ctx, status.RepoID, runIndex)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
// the run may be deleted manually, just skip it
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("GetRunByIndex: %w", err)
|
|
}
|
|
runMap[runIndex] = run
|
|
}
|
|
}
|
|
runs := make([]*actions_model.ActionRun, 0, len(runMap))
|
|
for _, run := range runMap {
|
|
runs = append(runs, run)
|
|
}
|
|
return runs, nil
|
|
}
|
|
|
|
func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, commitID string, _ error) {
|
|
switch run.Event {
|
|
case webhook_module.HookEventPush:
|
|
event = "push"
|
|
payload, err := run.GetPushEventPayload()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("GetPushEventPayload: %w", err)
|
|
}
|
|
if payload.HeadCommit == nil {
|
|
return "", "", errors.New("head commit is missing in event payload")
|
|
}
|
|
commitID = payload.HeadCommit.ID
|
|
case // pull_request
|
|
webhook_module.HookEventPullRequest,
|
|
webhook_module.HookEventPullRequestSync,
|
|
webhook_module.HookEventPullRequestAssign,
|
|
webhook_module.HookEventPullRequestLabel,
|
|
webhook_module.HookEventPullRequestReviewRequest,
|
|
webhook_module.HookEventPullRequestMilestone:
|
|
if run.TriggerEvent == actions_module.GithubEventPullRequestTarget {
|
|
event = "pull_request_target"
|
|
} else {
|
|
event = "pull_request"
|
|
}
|
|
payload, err := run.GetPullRequestEventPayload()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("GetPullRequestEventPayload: %w", err)
|
|
}
|
|
if payload.PullRequest == nil {
|
|
return "", "", errors.New("pull request is missing in event payload")
|
|
} else if payload.PullRequest.Head == nil {
|
|
return "", "", errors.New("head of pull request is missing in event payload")
|
|
}
|
|
commitID = payload.PullRequest.Head.Sha
|
|
case // pull_request_review events share the same PullRequestPayload as pull_request
|
|
webhook_module.HookEventPullRequestReviewApproved,
|
|
webhook_module.HookEventPullRequestReviewRejected,
|
|
webhook_module.HookEventPullRequestReviewComment:
|
|
event = run.TriggerEvent
|
|
payload, err := run.GetPullRequestEventPayload()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("GetPullRequestEventPayload: %w", err)
|
|
}
|
|
if payload.PullRequest == nil {
|
|
return "", "", errors.New("pull request is missing in event payload")
|
|
} else if payload.PullRequest.Head == nil {
|
|
return "", "", errors.New("head of pull request is missing in event payload")
|
|
}
|
|
commitID = payload.PullRequest.Head.Sha
|
|
case webhook_module.HookEventRelease:
|
|
event = string(run.Event)
|
|
commitID = run.CommitSHA
|
|
default: // do nothing, return empty
|
|
}
|
|
return event, commitID, nil
|
|
}
|
|
|
|
func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, commitID string, run *actions_model.ActionRun, job *actions_model.ActionRunJob) error {
|
|
// TODO: store workflow name as a field in ActionRun to avoid parsing
|
|
runName := path.Base(run.WorkflowID)
|
|
if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 {
|
|
runName = wfs[0].Name
|
|
}
|
|
ctxName := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
|
|
ctxName = strings.TrimSpace(ctxName) // git_model.NewCommitStatus also trims spaces
|
|
state := toCommitStatus(job.Status)
|
|
if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll); err == nil {
|
|
for _, v := range statuses {
|
|
if v.Context == ctxName {
|
|
if v.State == state {
|
|
// no need to update
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
return fmt.Errorf("GetLatestCommitStatus: %w", err)
|
|
}
|
|
|
|
var description string
|
|
switch job.Status {
|
|
// TODO: if we want support description in different languages, we need to support i18n placeholders in it
|
|
case actions_model.StatusSuccess:
|
|
description = fmt.Sprintf("Successful in %s", job.Duration())
|
|
case actions_model.StatusFailure:
|
|
description = fmt.Sprintf("Failing after %s", job.Duration())
|
|
case actions_model.StatusCancelled:
|
|
description = "Has been cancelled"
|
|
case actions_model.StatusSkipped:
|
|
description = "Has been skipped"
|
|
case actions_model.StatusRunning:
|
|
description = "Has started running"
|
|
case actions_model.StatusWaiting:
|
|
description = "Waiting to run"
|
|
case actions_model.StatusBlocked:
|
|
description = "Blocked by required conditions"
|
|
default:
|
|
description = "Unknown status: " + strconv.Itoa(int(job.Status))
|
|
}
|
|
|
|
index, err := getIndexOfJob(ctx, job)
|
|
if err != nil {
|
|
return fmt.Errorf("getIndexOfJob: %w", err)
|
|
}
|
|
|
|
creator := user_model.NewActionsUser()
|
|
status := git_model.CommitStatus{
|
|
SHA: commitID,
|
|
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
|
|
Description: description,
|
|
Context: ctxName,
|
|
CreatorID: creator.ID,
|
|
State: state,
|
|
}
|
|
|
|
return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID, &status)
|
|
}
|
|
|
|
func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState {
|
|
switch status {
|
|
case actions_model.StatusSuccess:
|
|
return commitstatus.CommitStatusSuccess
|
|
case actions_model.StatusFailure, actions_model.StatusCancelled:
|
|
return commitstatus.CommitStatusFailure
|
|
case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning:
|
|
return commitstatus.CommitStatusPending
|
|
case actions_model.StatusSkipped:
|
|
return commitstatus.CommitStatusSkipped
|
|
default:
|
|
return commitstatus.CommitStatusError
|
|
}
|
|
}
|
|
|
|
func getIndexOfJob(ctx context.Context, job *actions_model.ActionRunJob) (int, error) {
|
|
// TODO: store job index as a field in ActionRunJob to avoid this
|
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for i, v := range jobs {
|
|
if v.ID == job.ID {
|
|
return i, nil
|
|
}
|
|
}
|
|
return 0, nil
|
|
}
|