mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
6.9 KiB
215 lines
6.9 KiB
// 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 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 |
|
}
|
|
|