Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions models/git/commit_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,21 @@ import (

// CommitStatus holds a single Status of a single Commit
type CommitStatus struct {
ID int64 `xorm:"pk autoincr"`
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
Repo *repo_model.Repository `xorm:"-"`
State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
TargetURL string `xorm:"TEXT"`
Description string `xorm:"TEXT"`
ContextHash string `xorm:"VARCHAR(64) index"`
Context string `xorm:"TEXT"`
Creator *user_model.User `xorm:"-"`
ID int64 `xorm:"pk autoincr"`
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
Repo *repo_model.Repository `xorm:"-"`
State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`

// TargetURL points to the commit status page reported by a CI system
// If Gitea Actions is used, it is a relative link like "{RepoLink}/actions/runs/{RunID}/jobs{JobID}"
TargetURL string `xorm:"TEXT"`

Description string `xorm:"TEXT"`
ContextHash string `xorm:"VARCHAR(64) index"`
Context string `xorm:"TEXT"`
Creator *user_model.User `xorm:"-"`
CreatorID int64

CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
Expand Down Expand Up @@ -211,21 +215,45 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string {

// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
func (status *CommitStatus) HideActionsURL(ctx context.Context) {
if _, ok := status.cutTargetURLGiteaActionsPrefix(ctx); ok {
status.TargetURL = ""
}
}

func (status *CommitStatus) cutTargetURLGiteaActionsPrefix(ctx context.Context) (string, bool) {
if status.RepoID == 0 {
return
return "", false
}

if status.Repo == nil {
if err := status.loadRepository(ctx); err != nil {
log.Error("loadRepository: %v", err)
return
return "", false
}
}

prefix := status.Repo.Link() + "/actions"
if strings.HasPrefix(status.TargetURL, prefix) {
status.TargetURL = ""
return strings.CutPrefix(status.TargetURL, prefix)
}

// ParseGiteaActionsTargetURL parses the commit status target URL as Gitea Actions link
func (status *CommitStatus) ParseGiteaActionsTargetURL(ctx context.Context) (runID, jobID int64, ok bool) {
s, ok := status.cutTargetURLGiteaActionsPrefix(ctx)
if !ok {
return 0, 0, false
}

parts := strings.Split(s, "/") // expect: /runs/{runID}/jobs/{jobID}
if len(parts) < 5 || parts[1] != "runs" || parts[3] != "jobs" {
return 0, 0, false
}

runID, err1 := strconv.ParseInt(parts[2], 10, 64)
jobID, err2 := strconv.ParseInt(parts[4], 10, 64)
if err1 != nil || err2 != nil {
return 0, 0, false
}
return runID, jobID, true
}

// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
Expand Down
4 changes: 4 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,9 @@ pulls.status_checks_requested = Required
pulls.status_checks_details = Details
pulls.status_checks_hide_all = Hide all checks
pulls.status_checks_show_all = Show all checks
pulls.status_checks_approve_all = Approve all workflows
pulls.status_checks_need_approvals = %d workflow awaiting approval
pulls.status_checks_need_approvals_helper = The workflow will only run after approval from the repository maintainer.
pulls.update_branch = Update branch by merge
pulls.update_branch_rebase = Update branch by rebase
pulls.update_branch_success = Branch update was successful
Expand Down Expand Up @@ -3890,6 +3893,7 @@ workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event tri
workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger.

need_approval_desc = Need approval to run workflows for fork pull request.
approve_all_success = All workflow runs are approved successfully.

variables = Variables
variables.management = Variables Management
Expand Down
90 changes: 73 additions & 17 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,33 +606,53 @@ func Cancel(ctx *context_module.Context) {
func Approve(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)

current, jobs := getRunJobs(ctx, runIndex, -1)
approveRuns(ctx, []int64{runIndex})
if ctx.Written() {
return
}
run := current.Run

ctx.JSONOK()
}

func approveRuns(ctx *context_module.Context, runIndexes []int64) {
doer := ctx.Doer
repo := ctx.Repo.Repository

var updatedJobs []*actions_model.ActionRunJob
updatedJobs := make([]*actions_model.ActionRunJob, 0)
runMap := make(map[int64]*actions_model.ActionRun, len(runIndexes))
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIndexes))

err := db.WithTx(ctx, func(ctx context.Context) (err error) {
run.NeedApproval = false
run.ApprovedBy = doer.ID
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
return err
}
for _, job := range jobs {
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
for _, runIndex := range runIndexes {
run, err := actions_model.GetRunByIndex(ctx, repo.ID, runIndex)
if err != nil {
return err
}
if job.Status == actions_model.StatusWaiting {
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
runMap[run.ID] = run
run.Repo = repo
run.NeedApproval = false
run.ApprovedBy = doer.ID
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
return err
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
return err
}
runJobs[run.ID] = jobs
for _, job := range jobs {
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
if err != nil {
return err
}
if n > 0 {
updatedJobs = append(updatedJobs, job)
if job.Status == actions_model.StatusWaiting {
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
if n > 0 {
updatedJobs = append(updatedJobs, job)
}
}
}
}
Expand All @@ -643,7 +663,9 @@ func Approve(ctx *context_module.Context) {
return
}

actions_service.CreateCommitStatusForRunJobs(ctx, current.Run, jobs...)
for runID, run := range runMap {
actions_service.CreateCommitStatusForRunJobs(ctx, run, runJobs[runID]...)
}

if len(updatedJobs) > 0 {
job := updatedJobs[0]
Expand All @@ -654,8 +676,6 @@ func Approve(ctx *context_module.Context) {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}

ctx.JSONOK()
}

func Delete(ctx *context_module.Context) {
Expand Down Expand Up @@ -818,6 +838,42 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
}
}

func ApproveAllChecks(ctx *context_module.Context) {
repo := ctx.Repo.Repository
commitID := ctx.FormString("commit_id")

commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return
}
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
if err != nil {
ctx.ServerError("GetRunsFromCommitStatuses", err)
return
}

runIndexes := make([]int64, 0, len(runs))
for _, run := range runs {
if run.NeedApproval {
runIndexes = append(runIndexes, run.Index)
}
}

if len(runIndexes) == 0 {
ctx.JSONOK()
return
}

approveRuns(ctx, runIndexes)
if ctx.Written() {
return
}

ctx.Flash.Success(ctx.Tr("actions.approve_all_success"))
ctx.JSONOK()
}

func DisableWorkflowFile(ctx *context_module.Context) {
disableOrEnableWorkflowFile(ctx, false)
}
Expand Down
3 changes: 3 additions & 0 deletions routers/web/repo/issue_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,9 @@ func ViewIssue(ctx *context.Context) {

func ViewPullMergeBox(ctx *context.Context) {
issue := prepareIssueViewLoad(ctx)
if ctx.Written() {
return
}
if !issue.IsPull {
ctx.NotFound(nil)
return
Expand Down
32 changes: 30 additions & 2 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -311,6 +312,14 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
return compareInfo
}

type pullCommitStatusCheckData struct {
MissingRequiredChecks []string // list of missing required checks
IsContextRequired func(string) bool // function to check whether a context is required
RequireApprovalRunCount int // number of workflow runs that require approval
CanApprove bool // whether the user can approve workflow runs
ApproveLink string // link to approve all checks
}

// prepareViewPullInfo show meta information for a pull request preview page
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
Expand Down Expand Up @@ -456,6 +465,11 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
return nil
}

statusCheckData := &pullCommitStatusCheckData{
ApproveLink: fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), sha),
}
ctx.Data["StatusCheckData"] = statusCheckData

commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
Expand All @@ -465,6 +479,20 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
}

runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
if err != nil {
ctx.ServerError("GetRunsFromCommitStatuses", err)
return nil
}
for _, run := range runs {
if run.NeedApproval {
statusCheckData.RequireApprovalRunCount++
}
}
if statusCheckData.RequireApprovalRunCount > 0 {
statusCheckData.CanApprove = ctx.Repo.CanWrite(unit.TypeActions)
}

if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
Expand All @@ -486,9 +514,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
missingRequiredChecks = append(missingRequiredChecks, requiredContext)
}
}
ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
statusCheckData.MissingRequiredChecks = missingRequiredChecks

ctx.Data["is_context_required"] = func(context string) bool {
statusCheckData.IsContextRequired = func(context string) bool {
for _, c := range pb.StatusCheckContexts {
if c == context {
return true
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
m.Post("/run", reqRepoActionsWriter, actions.Run)
m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs)
m.Post("/approve-all-checks", reqRepoActionsWriter, actions.ApproveAllChecks)

m.Group("/runs/{run}", func() {
m.Combo("").
Expand Down
28 changes: 28 additions & 0 deletions services/actions/commit_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
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"

Expand Down Expand Up @@ -52,6 +53,33 @@ func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.Action
}
}

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:
Expand Down
3 changes: 1 addition & 2 deletions templates/repo/issue/view_content/pull_merge_box.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@
{{template "repo/pulls/status" (dict
"CommitStatus" .LatestCommitStatus
"CommitStatuses" .LatestCommitStatuses
"MissingRequiredChecks" .MissingRequiredChecks
"ShowHideChecks" true
"is_context_required" .is_context_required
"StatusCheckData" .StatusCheckData
)}}
</div>
{{end}}
Expand Down
Loading