summaryrefslogtreecommitdiffstats
path: root/services/actions/commit_status.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/actions/commit_status.go167
1 files changed, 167 insertions, 0 deletions
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
new file mode 100644
index 0000000..2698059
--- /dev/null
+++ b/services/actions/commit_status.go
@@ -0,0 +1,167 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "context"
+ "fmt"
+ "path"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ user_model "code.gitea.io/gitea/models/user"
+ actions_module "code.gitea.io/gitea/modules/actions"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
+ commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
+
+ "github.com/nektos/act/pkg/jobparser"
+)
+
+// 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 {
+ return fmt.Errorf("load run: %w", err)
+ }
+
+ run := job.Run
+
+ var (
+ sha string
+ event string
+ )
+ 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 fmt.Errorf("head commit is missing in event payload")
+ }
+ sha = payload.HeadCommit.ID
+ case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync:
+ 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 fmt.Errorf("pull request is missing in event payload")
+ } else if payload.PullRequest.Head == nil {
+ return fmt.Errorf("head of pull request is missing in event payload")
+ }
+ sha = payload.PullRequest.Head.Sha
+ case webhook_module.HookEventRelease:
+ event = string(run.Event)
+ sha = run.CommitSHA
+ default:
+ return nil
+ }
+
+ repo := run.Repo
+ // 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)
+ state := toCommitStatus(job.Status)
+ if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, 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)
+ }
+
+ 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"
+ }
+
+ index, err := getIndexOfJob(ctx, job)
+ if err != nil {
+ return fmt.Errorf("getIndexOfJob: %w", err)
+ }
+
+ creator := user_model.NewActionsUser()
+ if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator,
+ sha,
+ &git_model.CommitStatus{
+ SHA: sha,
+ TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
+ Description: description,
+ Context: ctxname,
+ CreatorID: creator.ID,
+ State: state,
+ }); err != nil {
+ return fmt.Errorf("NewCommitStatus: %w", err)
+ }
+
+ return nil
+}
+
+func toCommitStatus(status actions_model.Status) api.CommitStatusState {
+ switch status {
+ case actions_model.StatusSuccess, actions_model.StatusSkipped:
+ return api.CommitStatusSuccess
+ case actions_model.StatusFailure, actions_model.StatusCancelled:
+ return api.CommitStatusFailure
+ case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning:
+ return api.CommitStatusPending
+ default:
+ return api.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
+}