Skip to content

Commit 897e48d

Browse files
Add quick approve button on PR page (#35678)
This PR adds a quick approve button on PR page to allow reviewers to approve all pending checks. Only users with write permission to the Actions unit can approve. --------- Signed-off-by: Zettat123 <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 66ee8f3 commit 897e48d

File tree

10 files changed

+357
-47
lines changed

10 files changed

+357
-47
lines changed

models/git/commit_status.go

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,21 @@ import (
3030

3131
// CommitStatus holds a single Status of a single Commit
3232
type CommitStatus struct {
33-
ID int64 `xorm:"pk autoincr"`
34-
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
35-
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
36-
Repo *repo_model.Repository `xorm:"-"`
37-
State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
38-
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
39-
TargetURL string `xorm:"TEXT"`
40-
Description string `xorm:"TEXT"`
41-
ContextHash string `xorm:"VARCHAR(64) index"`
42-
Context string `xorm:"TEXT"`
43-
Creator *user_model.User `xorm:"-"`
33+
ID int64 `xorm:"pk autoincr"`
34+
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
35+
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
36+
Repo *repo_model.Repository `xorm:"-"`
37+
State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
38+
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
39+
40+
// TargetURL points to the commit status page reported by a CI system
41+
// If Gitea Actions is used, it is a relative link like "{RepoLink}/actions/runs/{RunID}/jobs{JobID}"
42+
TargetURL string `xorm:"TEXT"`
43+
44+
Description string `xorm:"TEXT"`
45+
ContextHash string `xorm:"VARCHAR(64) index"`
46+
Context string `xorm:"TEXT"`
47+
Creator *user_model.User `xorm:"-"`
4448
CreatorID int64
4549

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

212216
// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
213217
func (status *CommitStatus) HideActionsURL(ctx context.Context) {
218+
if _, ok := status.cutTargetURLGiteaActionsPrefix(ctx); ok {
219+
status.TargetURL = ""
220+
}
221+
}
222+
223+
func (status *CommitStatus) cutTargetURLGiteaActionsPrefix(ctx context.Context) (string, bool) {
214224
if status.RepoID == 0 {
215-
return
225+
return "", false
216226
}
217227

218228
if status.Repo == nil {
219229
if err := status.loadRepository(ctx); err != nil {
220230
log.Error("loadRepository: %v", err)
221-
return
231+
return "", false
222232
}
223233
}
224234

225235
prefix := status.Repo.Link() + "/actions"
226-
if strings.HasPrefix(status.TargetURL, prefix) {
227-
status.TargetURL = ""
236+
return strings.CutPrefix(status.TargetURL, prefix)
237+
}
238+
239+
// ParseGiteaActionsTargetURL parses the commit status target URL as Gitea Actions link
240+
func (status *CommitStatus) ParseGiteaActionsTargetURL(ctx context.Context) (runID, jobID int64, ok bool) {
241+
s, ok := status.cutTargetURLGiteaActionsPrefix(ctx)
242+
if !ok {
243+
return 0, 0, false
244+
}
245+
246+
parts := strings.Split(s, "/") // expect: /runs/{runID}/jobs/{jobID}
247+
if len(parts) < 5 || parts[1] != "runs" || parts[3] != "jobs" {
248+
return 0, 0, false
249+
}
250+
251+
runID, err1 := strconv.ParseInt(parts[2], 10, 64)
252+
jobID, err2 := strconv.ParseInt(parts[4], 10, 64)
253+
if err1 != nil || err2 != nil {
254+
return 0, 0, false
228255
}
256+
return runID, jobID, true
229257
}
230258

231259
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc

options/locale/locale_en-US.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,6 +1969,9 @@ pulls.status_checks_requested = Required
19691969
pulls.status_checks_details = Details
19701970
pulls.status_checks_hide_all = Hide all checks
19711971
pulls.status_checks_show_all = Show all checks
1972+
pulls.status_checks_approve_all = Approve all workflows
1973+
pulls.status_checks_need_approvals = %d workflow awaiting approval
1974+
pulls.status_checks_need_approvals_helper = The workflow will only run after approval from the repository maintainer.
19721975
pulls.update_branch = Update branch by merge
19731976
pulls.update_branch_rebase = Update branch by rebase
19741977
pulls.update_branch_success = Branch update was successful
@@ -3890,6 +3893,7 @@ workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event tri
38903893
workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger.
38913894

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

38943898
variables = Variables
38953899
variables.management = Variables Management

routers/web/repo/actions/view.go

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -606,33 +606,53 @@ func Cancel(ctx *context_module.Context) {
606606
func Approve(ctx *context_module.Context) {
607607
runIndex := getRunIndex(ctx)
608608

609-
current, jobs := getRunJobs(ctx, runIndex, -1)
609+
approveRuns(ctx, []int64{runIndex})
610610
if ctx.Written() {
611611
return
612612
}
613-
run := current.Run
613+
614+
ctx.JSONOK()
615+
}
616+
617+
func approveRuns(ctx *context_module.Context, runIndexes []int64) {
614618
doer := ctx.Doer
619+
repo := ctx.Repo.Repository
615620

616-
var updatedJobs []*actions_model.ActionRunJob
621+
updatedJobs := make([]*actions_model.ActionRunJob, 0)
622+
runMap := make(map[int64]*actions_model.ActionRun, len(runIndexes))
623+
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIndexes))
617624

618625
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
619-
run.NeedApproval = false
620-
run.ApprovedBy = doer.ID
621-
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
622-
return err
623-
}
624-
for _, job := range jobs {
625-
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
626+
for _, runIndex := range runIndexes {
627+
run, err := actions_model.GetRunByIndex(ctx, repo.ID, runIndex)
626628
if err != nil {
627629
return err
628630
}
629-
if job.Status == actions_model.StatusWaiting {
630-
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
631+
runMap[run.ID] = run
632+
run.Repo = repo
633+
run.NeedApproval = false
634+
run.ApprovedBy = doer.ID
635+
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
636+
return err
637+
}
638+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
639+
if err != nil {
640+
return err
641+
}
642+
runJobs[run.ID] = jobs
643+
for _, job := range jobs {
644+
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
631645
if err != nil {
632646
return err
633647
}
634-
if n > 0 {
635-
updatedJobs = append(updatedJobs, job)
648+
if job.Status == actions_model.StatusWaiting {
649+
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
650+
if err != nil {
651+
return err
652+
}
653+
if n > 0 {
654+
updatedJobs = append(updatedJobs, job)
655+
}
636656
}
637657
}
638658
}
@@ -643,7 +663,9 @@ func Approve(ctx *context_module.Context) {
643663
return
644664
}
645665

646-
actions_service.CreateCommitStatusForRunJobs(ctx, current.Run, jobs...)
666+
for runID, run := range runMap {
667+
actions_service.CreateCommitStatusForRunJobs(ctx, run, runJobs[runID]...)
668+
}
647669

648670
if len(updatedJobs) > 0 {
649671
job := updatedJobs[0]
@@ -654,8 +676,6 @@ func Approve(ctx *context_module.Context) {
654676
_ = job.LoadAttributes(ctx)
655677
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
656678
}
657-
658-
ctx.JSONOK()
659679
}
660680

661681
func Delete(ctx *context_module.Context) {
@@ -818,6 +838,42 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
818838
}
819839
}
820840

841+
func ApproveAllChecks(ctx *context_module.Context) {
842+
repo := ctx.Repo.Repository
843+
commitID := ctx.FormString("commit_id")
844+
845+
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
846+
if err != nil {
847+
ctx.ServerError("GetLatestCommitStatus", err)
848+
return
849+
}
850+
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
851+
if err != nil {
852+
ctx.ServerError("GetRunsFromCommitStatuses", err)
853+
return
854+
}
855+
856+
runIndexes := make([]int64, 0, len(runs))
857+
for _, run := range runs {
858+
if run.NeedApproval {
859+
runIndexes = append(runIndexes, run.Index)
860+
}
861+
}
862+
863+
if len(runIndexes) == 0 {
864+
ctx.JSONOK()
865+
return
866+
}
867+
868+
approveRuns(ctx, runIndexes)
869+
if ctx.Written() {
870+
return
871+
}
872+
873+
ctx.Flash.Success(ctx.Tr("actions.approve_all_success"))
874+
ctx.JSONOK()
875+
}
876+
821877
func DisableWorkflowFile(ctx *context_module.Context) {
822878
disableOrEnableWorkflowFile(ctx, false)
823879
}

routers/web/repo/issue_view.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,9 @@ func ViewIssue(ctx *context.Context) {
437437

438438
func ViewPullMergeBox(ctx *context.Context) {
439439
issue := prepareIssueViewLoad(ctx)
440+
if ctx.Written() {
441+
return
442+
}
440443
if !issue.IsPull {
441444
ctx.NotFound(nil)
442445
return

routers/web/repo/pull.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"code.gitea.io/gitea/modules/web"
3939
"code.gitea.io/gitea/routers/utils"
4040
shared_user "code.gitea.io/gitea/routers/web/shared/user"
41+
actions_service "code.gitea.io/gitea/services/actions"
4142
asymkey_service "code.gitea.io/gitea/services/asymkey"
4243
"code.gitea.io/gitea/services/automerge"
4344
"code.gitea.io/gitea/services/context"
@@ -311,6 +312,14 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
311312
return compareInfo
312313
}
313314

315+
type pullCommitStatusCheckData struct {
316+
MissingRequiredChecks []string // list of missing required checks
317+
IsContextRequired func(string) bool // function to check whether a context is required
318+
RequireApprovalRunCount int // number of workflow runs that require approval
319+
CanApprove bool // whether the user can approve workflow runs
320+
ApproveLink string // link to approve all checks
321+
}
322+
314323
// prepareViewPullInfo show meta information for a pull request preview page
315324
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
316325
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
@@ -456,6 +465,11 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
456465
return nil
457466
}
458467

468+
statusCheckData := &pullCommitStatusCheckData{
469+
ApproveLink: fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), sha),
470+
}
471+
ctx.Data["StatusCheckData"] = statusCheckData
472+
459473
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
460474
if err != nil {
461475
ctx.ServerError("GetLatestCommitStatus", err)
@@ -465,6 +479,20 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
465479
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
466480
}
467481

482+
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
483+
if err != nil {
484+
ctx.ServerError("GetRunsFromCommitStatuses", err)
485+
return nil
486+
}
487+
for _, run := range runs {
488+
if run.NeedApproval {
489+
statusCheckData.RequireApprovalRunCount++
490+
}
491+
}
492+
if statusCheckData.RequireApprovalRunCount > 0 {
493+
statusCheckData.CanApprove = ctx.Repo.CanWrite(unit.TypeActions)
494+
}
495+
468496
if len(commitStatuses) > 0 {
469497
ctx.Data["LatestCommitStatuses"] = commitStatuses
470498
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
@@ -486,9 +514,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
486514
missingRequiredChecks = append(missingRequiredChecks, requiredContext)
487515
}
488516
}
489-
ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
517+
statusCheckData.MissingRequiredChecks = missingRequiredChecks
490518

491-
ctx.Data["is_context_required"] = func(context string) bool {
519+
statusCheckData.IsContextRequired = func(context string) bool {
492520
for _, c := range pb.StatusCheckContexts {
493521
if c == context {
494522
return true

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,7 @@ func registerWebRoutes(m *web.Router) {
14591459
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
14601460
m.Post("/run", reqRepoActionsWriter, actions.Run)
14611461
m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs)
1462+
m.Post("/approve-all-checks", reqRepoActionsWriter, actions.ApproveAllChecks)
14621463

14631464
m.Group("/runs/{run}", func() {
14641465
m.Combo("").

services/actions/commit_status.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
actions_module "code.gitea.io/gitea/modules/actions"
1919
"code.gitea.io/gitea/modules/commitstatus"
2020
"code.gitea.io/gitea/modules/log"
21+
"code.gitea.io/gitea/modules/util"
2122
webhook_module "code.gitea.io/gitea/modules/webhook"
2223
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
2324

@@ -52,6 +53,33 @@ func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.Action
5253
}
5354
}
5455

56+
func GetRunsFromCommitStatuses(ctx context.Context, statuses []*git_model.CommitStatus) ([]*actions_model.ActionRun, error) {
57+
runMap := make(map[int64]*actions_model.ActionRun)
58+
for _, status := range statuses {
59+
runIndex, _, ok := status.ParseGiteaActionsTargetURL(ctx)
60+
if !ok {
61+
continue
62+
}
63+
_, ok = runMap[runIndex]
64+
if !ok {
65+
run, err := actions_model.GetRunByIndex(ctx, status.RepoID, runIndex)
66+
if err != nil {
67+
if errors.Is(err, util.ErrNotExist) {
68+
// the run may be deleted manually, just skip it
69+
continue
70+
}
71+
return nil, fmt.Errorf("GetRunByIndex: %w", err)
72+
}
73+
runMap[runIndex] = run
74+
}
75+
}
76+
runs := make([]*actions_model.ActionRun, 0, len(runMap))
77+
for _, run := range runMap {
78+
runs = append(runs, run)
79+
}
80+
return runs, nil
81+
}
82+
5583
func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, commitID string, _ error) {
5684
switch run.Event {
5785
case webhook_module.HookEventPush:

templates/repo/issue/view_content/pull_merge_box.tmpl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131
{{template "repo/pulls/status" (dict
3232
"CommitStatus" .LatestCommitStatus
3333
"CommitStatuses" .LatestCommitStatuses
34-
"MissingRequiredChecks" .MissingRequiredChecks
3534
"ShowHideChecks" true
36-
"is_context_required" .is_context_required
35+
"StatusCheckData" .StatusCheckData
3736
)}}
3837
</div>
3938
{{end}}

0 commit comments

Comments
 (0)