Refactor commit status for Actions jobs (#23786) (#24060)

Backport #23786

Refactor commit status for Actions jobs (#23786)

Highlights:
- Treat `StatusSkipped` as `CommitStatusSuccess` instead of
`CommitStatusFailure`, so it fixed #23599.
- Use the bot user `gitea-actions` instead of the trigger as the creator
of commit status.
- New format `<run_name> / <job_name> / (<event>)` for the context of
commit status to avoid conflicts.
- Add descriptions for commit status.
- Add the missing calls to `CreateCommitStatus`.
- Refactor `CreateCommitStatus` to make it easier to use.

Co-authored-by: Jason Song <i@wolfogre.com>
This commit is contained in:
Denys Konovalov 2023-04-12 04:37:34 +02:00 committed by GitHub
parent 580da8f353
commit 29724f31c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 74 additions and 71 deletions

View file

@ -149,10 +149,7 @@ func (s *Service) UpdateTask(
return nil, status.Errorf(codes.Internal, "load job: %v", err) return nil, status.Errorf(codes.Internal, "load job: %v", err)
} }
if err := actions_service.CreateCommitStatus(ctx, task.Job); err != nil { actions_service.CreateCommitStatus(ctx, task.Job)
log.Error("Update commit status for job %v failed: %v", task.Job.ID, err)
// go on
}
if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED { if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil { if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil {

View file

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
secret_module "code.gitea.io/gitea/modules/secret" secret_module "code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/actions"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1" runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
@ -28,6 +29,8 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
return nil, false, nil return nil, false, nil
} }
actions.CreateCommitStatus(ctx, t.Job)
task := &runnerv1.Task{ task := &runnerv1.Task{
Id: t.ID, Id: t.ID,
WorkflowPayload: t.Job.WorkflowPayload, WorkflowPayload: t.Job.WorkflowPayload,

View file

@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/actions"
context_module "code.gitea.io/gitea/modules/context" context_module "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -215,10 +214,7 @@ func Rerun(ctx *context_module.Context) {
return return
} }
if err := actions_service.CreateCommitStatus(ctx, job); err != nil { actions_service.CreateCommitStatus(ctx, job)
log.Error("Update commit status for job %v failed: %v", job.ID, err)
// go on
}
ctx.JSON(http.StatusOK, struct{}{}) ctx.JSON(http.StatusOK, struct{}{})
} }
@ -259,12 +255,7 @@ func Cancel(ctx *context_module.Context) {
return return
} }
for _, job := range jobs { actions_service.CreateCommitStatus(ctx, jobs...)
if err := actions_service.CreateCommitStatus(ctx, job); err != nil {
log.Error("Update commit status for job %v failed: %v", job.ID, err)
// go on
}
}
ctx.JSON(http.StatusOK, struct{}{}) ctx.JSON(http.StatusOK, struct{}{})
} }

View file

@ -64,12 +64,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
} }
} }
for _, job := range jobs { CreateCommitStatus(ctx, jobs...)
if err := CreateCommitStatus(ctx, job); err != nil {
log.Error("Update commit status for job %v failed: %v", job.ID, err)
// go on
}
}
return nil return nil
} }
@ -96,10 +91,7 @@ func CancelAbandonedJobs(ctx context.Context) error {
log.Warn("cancel abandoned job %v: %v", job.ID, err) log.Warn("cancel abandoned job %v: %v", job.ID, err)
// go on // go on
} }
if err := CreateCommitStatus(ctx, job); err != nil { CreateCommitStatus(ctx, job)
log.Error("Update commit status for job %v failed: %v", job.ID, err)
// go on
}
} }
return nil return nil

View file

@ -6,79 +6,80 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"path"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/nektos/act/pkg/jobparser"
) )
func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) error { // CreateCommitStatus creates a commit status for the given job.
// It won't return an error failed, but will log it, because it's not critical.
func CreateCommitStatus(ctx context.Context, jobs ...*actions_model.ActionRunJob) {
for _, job := range jobs {
if err := createCommitStatus(ctx, job); err != nil {
log.Error("Failed to create commit status for job %d: %v", job.ID, err)
}
}
}
func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) error {
if err := job.LoadAttributes(ctx); err != nil { if err := job.LoadAttributes(ctx); err != nil {
return fmt.Errorf("load run: %w", err) return fmt.Errorf("load run: %w", err)
} }
run := job.Run run := job.Run
var (
sha string
creatorID int64
)
var (
sha string
event string
)
switch run.Event { switch run.Event {
case webhook_module.HookEventPush: case webhook_module.HookEventPush:
event = "push"
payload, err := run.GetPushEventPayload() payload, err := run.GetPushEventPayload()
if err != nil { if err != nil {
return fmt.Errorf("GetPushEventPayload: %w", err) return fmt.Errorf("GetPushEventPayload: %w", err)
} }
if payload.HeadCommit == nil {
// Since the payload comes from json data, we should check if it's broken, or it will cause panic
switch {
case payload.Repo == nil:
return fmt.Errorf("repo is missing in event payload")
case payload.Pusher == nil:
return fmt.Errorf("pusher is missing in event payload")
case payload.HeadCommit == nil:
return fmt.Errorf("head commit is missing in event payload") return fmt.Errorf("head commit is missing in event payload")
} }
sha = payload.HeadCommit.ID sha = payload.HeadCommit.ID
creatorID = payload.Pusher.ID
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync: case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync:
event = "pull_request"
payload, err := run.GetPullRequestEventPayload() payload, err := run.GetPullRequestEventPayload()
if err != nil { if err != nil {
return fmt.Errorf("GetPullRequestEventPayload: %w", err) return fmt.Errorf("GetPullRequestEventPayload: %w", err)
} }
if payload.PullRequest == nil {
switch {
case payload.PullRequest == nil:
return fmt.Errorf("pull request is missing in event payload") return fmt.Errorf("pull request is missing in event payload")
case payload.PullRequest.Head == nil: } else if payload.PullRequest.Head == nil {
return fmt.Errorf("head of pull request is missing in event payload") return fmt.Errorf("head of pull request is missing in event payload")
case payload.PullRequest.Head.Repository == nil:
return fmt.Errorf("head repository of pull request is missing in event payload")
case payload.PullRequest.Head.Repository.Owner == nil:
return fmt.Errorf("owner of head repository of pull request is missing in evnt payload")
} }
sha = payload.PullRequest.Head.Sha sha = payload.PullRequest.Head.Sha
creatorID = payload.PullRequest.Head.Repository.Owner.ID
default: default:
return nil return nil
} }
repo := run.Repo repo := run.Repo
ctxname := job.Name // TODO: store workflow name as a field in ActionRun to avoid parsing
state := toCommitStatus(job.Status) runName := path.Base(run.WorkflowID)
creator, err := user_model.GetUserByID(ctx, creatorID) if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 {
if err != nil { runName = wfs[0].Name
return fmt.Errorf("GetUserByID: %w", err)
} }
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
state := toCommitStatus(job.Status)
if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{}); err == nil { if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{}); err == nil {
for _, v := range statuses { for _, v := range statuses {
if v.Context == ctxname { if v.Context == ctxname {
if v.State == state { if v.State == state {
// no need to update
return nil return nil
} }
break break
@ -93,6 +94,26 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return fmt.Errorf("getIndexOfJob: %w", err) return fmt.Errorf("getIndexOfJob: %w", err)
} }
description := ""
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"
}
creator := user_model.NewActionsUser()
if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
Repo: repo, Repo: repo,
SHA: sha, SHA: sha,
@ -100,9 +121,9 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
CommitStatus: &git_model.CommitStatus{ CommitStatus: &git_model.CommitStatus{
SHA: sha, SHA: sha,
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index), TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
Description: "", Description: description,
Context: ctxname, Context: ctxname,
CreatorID: creatorID, CreatorID: creator.ID,
State: state, State: state,
}, },
}); err != nil { }); err != nil {
@ -114,9 +135,9 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
func toCommitStatus(status actions_model.Status) api.CommitStatusState { func toCommitStatus(status actions_model.Status) api.CommitStatusState {
switch status { switch status {
case actions_model.StatusSuccess: case actions_model.StatusSuccess, actions_model.StatusSkipped:
return api.CommitStatusSuccess return api.CommitStatusSuccess
case actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped: case actions_model.StatusFailure, actions_model.StatusCancelled:
return api.CommitStatusFailure return api.CommitStatusFailure
case actions_model.StatusWaiting, actions_model.StatusBlocked: case actions_model.StatusWaiting, actions_model.StatusBlocked:
return api.CommitStatusPending return api.CommitStatusPending

View file

@ -45,11 +45,11 @@ func jobEmitterQueueHandle(data ...queue.Data) []queue.Data {
} }
func checkJobsOfRun(ctx context.Context, runID int64) error { func checkJobsOfRun(ctx context.Context, runID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error { jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: runID})
jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: runID}) if err != nil {
if err != nil { return err
return err }
} if err := db.WithTx(ctx, func(ctx context.Context) error {
idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
for _, job := range jobs { for _, job := range jobs {
idToJobs[job.JobID] = append(idToJobs[job.JobID], job) idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
@ -67,7 +67,11 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
} }
} }
return nil return nil
}) }); err != nil {
return err
}
CreateCommitStatus(ctx, jobs...)
return nil
} }
type jobStatusResolver struct { type jobStatusResolver struct {

View file

@ -193,12 +193,7 @@ func notify(ctx context.Context, input *notifyInput) error {
if jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}); err != nil { if jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}); err != nil {
log.Error("FindRunJobs: %v", err) log.Error("FindRunJobs: %v", err)
} else { } else {
for _, job := range jobs { CreateCommitStatus(ctx, jobs...)
if err := CreateCommitStatus(ctx, job); err != nil {
log.Error("Update commit status for job %v failed: %v", job.ID, err)
// go on
}
}
} }
} }