mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-20 20:34:24 -04:00
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 <zettat123@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -36,7 +36,11 @@ type CommitStatus struct {
|
||||
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"`
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
@@ -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
|
||||
|
@@ -606,21 +606,40 @@ 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
|
||||
doer := ctx.Doer
|
||||
|
||||
var updatedJobs []*actions_model.ActionRunJob
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func approveRuns(ctx *context_module.Context, runIndexes []int64) {
|
||||
doer := ctx.Doer
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
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) {
|
||||
for _, runIndex := range runIndexes {
|
||||
run, err := actions_model.GetRunByIndex(ctx, repo.ID, runIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
@@ -636,6 +655,7 @@ func Approve(ctx *context_module.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -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]
|
||||
@@ -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) {
|
||||
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
@@ -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("").
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
@@ -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}}
|
||||
|
@@ -1,11 +1,10 @@
|
||||
{{/* Template Attributes:
|
||||
* CommitStatus: summary of all commit status state
|
||||
* CommitStatuses: all commit status elements
|
||||
* MissingRequiredChecks: commit check contexts that are required by branch protection but not present
|
||||
* ShowHideChecks: whether use a button to show/hide the checks
|
||||
* is_context_required: Used in pull request commit status check table
|
||||
* StatusCheckData: additional status check data, see backend pullCommitStatusCheckData struct
|
||||
*/}}
|
||||
|
||||
{{$statusCheckData := .StatusCheckData}}
|
||||
{{if .CommitStatus}}
|
||||
<div class="commit-status-panel">
|
||||
<div class="ui top attached header commit-status-header">
|
||||
@@ -33,26 +32,44 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if and $statusCheckData $statusCheckData.RequireApprovalRunCount}}
|
||||
<div class="ui attached segment flex-text-block tw-justify-between" id="approve-status-checks">
|
||||
<div>
|
||||
<strong>
|
||||
{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals" $statusCheckData.RequireApprovalRunCount}}
|
||||
</strong>
|
||||
<p>{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals_helper"}}</p>
|
||||
</div>
|
||||
{{if $statusCheckData.CanApprove}}
|
||||
<button class="ui basic button link-action" data-url="{{$statusCheckData.ApproveLink}}">
|
||||
{{ctx.Locale.Tr "repo.pulls.status_checks_approve_all"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="commit-status-list">
|
||||
{{range .CommitStatuses}}
|
||||
<div class="commit-status-item">
|
||||
{{template "repo/commit_status" .}}
|
||||
<div class="status-context gt-ellipsis">{{.Context}} <span class="text light-2">{{.Description}}</span></div>
|
||||
<div class="ui status-details">
|
||||
{{if $.is_context_required}}
|
||||
{{if (call $.is_context_required .Context)}}<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>{{end}}
|
||||
{{if and $statusCheckData $statusCheckData.IsContextRequired}}
|
||||
{{if (call $statusCheckData.IsContextRequired .Context)}}<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>{{end}}
|
||||
{{end}}
|
||||
<span>{{if .TargetURL}}<a href="{{.TargetURL}}">{{ctx.Locale.Tr "repo.pulls.status_checks_details"}}</a>{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range .MissingRequiredChecks}}
|
||||
{{if $statusCheckData}}
|
||||
{{range $statusCheckData.MissingRequiredChecks}}
|
||||
<div class="commit-status-item">
|
||||
{{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}}
|
||||
<div class="status-context gt-ellipsis">{{.}}</div>
|
||||
<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
146
tests/integration/actions_approve_test.go
Normal file
146
tests/integration/actions_approve_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApproveAllRunsOnPullRequestPage(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user2 is the owner of the base repo
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
// user4 is the owner of the fork repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Session := loginUser(t, user4.Name)
|
||||
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "approve-all-runs", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(user2APICtx)(t)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// init workflows
|
||||
wf1TreePath := ".gitea/workflows/pull_1.yml"
|
||||
wf1FileContent := `name: Pull 1
|
||||
on: pull_request
|
||||
jobs:
|
||||
unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo unit-test
|
||||
`
|
||||
opts1 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf1TreePath, opts1)
|
||||
wf2TreePath := ".gitea/workflows/pull_2.yml"
|
||||
wf2FileContent := `name: Pull 2
|
||||
on: pull_request
|
||||
jobs:
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo integration-test
|
||||
`
|
||||
opts2 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf2TreePath, opts2)
|
||||
|
||||
// user4 forks the repo
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{
|
||||
Name: util.ToPointer("approve-all-runs-fork"),
|
||||
}).AddTokenAuth(user4Token)
|
||||
resp := MakeRequest(t, req, http.StatusAccepted)
|
||||
var apiForkRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiForkRepo)
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
|
||||
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(user4APICtx)(t)
|
||||
|
||||
// user4 creates a pull request from branch "bugfix/user4"
|
||||
doAPICreateFile(user4APICtx, "user4-fix.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "bugfix/user4",
|
||||
Message: "create user4-fix.txt",
|
||||
Author: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix")),
|
||||
})(t)
|
||||
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":bugfix/user4")(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check runs
|
||||
run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_1.yml"})
|
||||
assert.True(t, run1.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusBlocked, run1.Status)
|
||||
run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_2.yml"})
|
||||
assert.True(t, run2.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusBlocked, run2.Status)
|
||||
|
||||
// user4 cannot see the approve button
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
|
||||
resp = user4Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
assert.Zero(t, htmlDoc.doc.Find("#approve-status-checks button.link-action").Length())
|
||||
|
||||
// user2 can see the approve button
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
dataURL, exist := htmlDoc.doc.Find("#approve-status-checks button.link-action").Attr("data-url")
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s",
|
||||
baseRepo.Link(), apiPull.Head.Sha),
|
||||
dataURL,
|
||||
)
|
||||
|
||||
// user2 approves all runs
|
||||
req = NewRequestWithValues(t, "POST", dataURL,
|
||||
map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, user2Session),
|
||||
})
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// check runs
|
||||
run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID})
|
||||
assert.False(t, run1.NeedApproval)
|
||||
assert.Equal(t, user2.ID, run1.ApprovedBy)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run1.Status)
|
||||
run2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID})
|
||||
assert.False(t, run2.NeedApproval)
|
||||
assert.Equal(t, user2.ID, run2.ApprovedBy)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run2.Status)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user