mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-09 07:12:11 +00:00
Backport of 8e85454a50
Fixes the issue that status report always shows "waiting to run" when
already running, by comparing state, targetURL, and description (not
just state) in the deduplication check.
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (claude-sonnet-4-5) <noreply@anthropic.com>
Agent-Logs-Url: https://github.com/go-gitea/gitea/sessions/1b315e9f-698c-4564-9ffa-aac1b9cb1692
Co-authored-by: silverwind <115237+silverwind@users.noreply.github.com>
212 lines
7.2 KiB
Go
212 lines
7.2 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"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/actions/jobparser"
|
|
"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"
|
|
)
|
|
|
|
// 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 {
|
|
runID, _, ok := status.ParseGiteaActionsTargetURL(ctx)
|
|
if !ok {
|
|
continue
|
|
}
|
|
_, ok = runMap[runID]
|
|
if !ok {
|
|
run, err := actions_model.GetRunByRepoAndID(ctx, status.RepoID, runID)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
// the run may be deleted manually, just skip it
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("GetRunByRepoAndID: %w", err)
|
|
}
|
|
runMap[runID] = 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 := strings.TrimSpace(fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)) // git_model.NewCommitStatus also trims spaces
|
|
state := toCommitStatus(job.Status)
|
|
targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID)
|
|
description := toCommitStatusDescription(job)
|
|
|
|
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
|
|
if err != nil {
|
|
return fmt.Errorf("GetLatestCommitStatus: %w", err)
|
|
}
|
|
for _, v := range statuses {
|
|
if v.Context == ctxName {
|
|
if v.State == state && v.TargetURL == targetURL && v.Description == description {
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
creator := user_model.NewActionsUser()
|
|
status := git_model.CommitStatus{
|
|
SHA: commitID,
|
|
TargetURL: targetURL,
|
|
Description: description,
|
|
Context: ctxName,
|
|
State: state,
|
|
CreatorID: creator.ID,
|
|
}
|
|
|
|
return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID, &status)
|
|
}
|
|
|
|
func toCommitStatusDescription(job *actions_model.ActionRunJob) 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:
|
|
return fmt.Sprintf("Successful in %s", job.Duration())
|
|
case actions_model.StatusFailure:
|
|
return fmt.Sprintf("Failing after %s", job.Duration())
|
|
case actions_model.StatusCancelled:
|
|
return "Has been cancelled"
|
|
case actions_model.StatusSkipped:
|
|
return "Has been skipped"
|
|
case actions_model.StatusRunning:
|
|
return "Has started running"
|
|
case actions_model.StatusWaiting:
|
|
return "Waiting to run"
|
|
case actions_model.StatusBlocked:
|
|
return "Blocked by required conditions"
|
|
default:
|
|
return fmt.Sprintf("Unknown status: %d", job.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
|
|
}
|
|
}
|