feat: UI for the pull request trust management panel
See the documentation pull request for a description https://codeberg.org/forgejo/docs/pulls/1567 https://forgejo.codeberg.page/@docs_pull_1567/docs/next/user/actions/security-pull-request/
This commit is contained in:
@@ -56,6 +56,19 @@
|
||||
"relativetime.2months": "two months ago",
|
||||
"relativetime.1year": "last year",
|
||||
"relativetime.2years": "two years ago",
|
||||
"repo.pulls.poster_manage_approval": "Manage approval",
|
||||
"repo.pulls.poster_requires_approval": "Some workflows are <a href=\"%[1]s\">waiting to be reviewed.</a>",
|
||||
"repo.pulls.poster_requires_approval.tooltip": "The author of this pull request is not trusted to run workflows triggered by a pull request created from a forked repository or with AGit. The workflows triggered by a `pull_request` event will not run until they are approved.",
|
||||
"repo.pulls.poster_is_trusted": "The author of this pull request is <a href=\"%[1]s\">always trusted to run workflows.</a>",
|
||||
"repo.pulls.poster_is_trusted.tooltip": "The author of this pull request is explicitly trusted to run workflows triggered by `pull_request` events.",
|
||||
"repo.pulls.poster_trust_deny": "Deny",
|
||||
"repo.pulls.poster_trust_deny.tooltip": "The workflows waiting approval will be canceled.",
|
||||
"repo.pulls.poster_trust_once": "Approve once",
|
||||
"repo.pulls.poster_trust_once.tooltip": "The workflows triggered by a `pull_request` event will run on this commit but will need to be approved for all future commits pushed to this pull request.",
|
||||
"repo.pulls.poster_trust_always": "Approve always",
|
||||
"repo.pulls.poster_trust_always.tooltip": "The workflows triggered by a `pull_request` event will run on this commit and there will be no need to approve runs from this pull request or future pull requests authored by the same user.",
|
||||
"repo.pulls.poster_trust_revoke": "Revoke",
|
||||
"repo.pulls.poster_trust_revoke.tooltip": "The author of this pull request will no longer be trusted to run workflows triggered by a `pull_request` event, each run will have to be manually approved.",
|
||||
"repo.pulls.already_merged": "Merge failed: This pull request has already been merged.",
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s",
|
||||
@@ -137,6 +150,7 @@
|
||||
"admin.auths.allow_username_change.description": "Allow users to change their username in the profile settings",
|
||||
"admin.dashboard.cleanup_offline_runners": "Cleanup offline runners",
|
||||
"admin.dashboard.remove_resolved_reports": "Remove resolved reports",
|
||||
"admin.dashboard.actions_action_user": "Revoke Forgejo Actions trust for inactive users",
|
||||
"admin.config.security": "Security configuration",
|
||||
"admin.config.global_2fa_requirement.title": "Global two-factor requirement",
|
||||
"admin.config.global_2fa_requirement.none": "No",
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"forgejo.org/models"
|
||||
actions_model "forgejo.org/models/actions"
|
||||
activities_model "forgejo.org/models/activities"
|
||||
asymkey_model "forgejo.org/models/asymkey"
|
||||
"forgejo.org/models/db"
|
||||
@@ -44,6 +45,7 @@ import (
|
||||
"forgejo.org/modules/util"
|
||||
"forgejo.org/modules/web"
|
||||
"forgejo.org/routers/utils"
|
||||
actions_service "forgejo.org/services/actions"
|
||||
asymkey_service "forgejo.org/services/asymkey"
|
||||
"forgejo.org/services/automerge"
|
||||
"forgejo.org/services/context"
|
||||
@@ -764,6 +766,11 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
|
||||
ctx.Data["IsNothingToCompare"] = true
|
||||
}
|
||||
|
||||
PrepareViewPullInfoActions(ctx, pull)
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pull.IsWorkInProgress(ctx) {
|
||||
ctx.Data["IsPullWorkInProgress"] = true
|
||||
ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx)
|
||||
@@ -1943,3 +1950,67 @@ func SetAllowEdits(ctx *context.Context) {
|
||||
"allow_maintainer_edit": pr.AllowMaintainerEdit,
|
||||
})
|
||||
}
|
||||
|
||||
func PrepareViewPullInfoActions(ctx *context.Context, pull *issues_model.PullRequest) {
|
||||
canReadUnitActions := ctx.Repo.CanRead(unit.TypeActions)
|
||||
ctx.Data["CanReadUnitActions"] = canReadUnitActions
|
||||
|
||||
if !canReadUnitActions {
|
||||
return
|
||||
}
|
||||
|
||||
PrepareViewPullInfoActionsTrust(ctx, pull)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func PrepareViewPullInfoActionsTrust(ctx *context.Context, pull *issues_model.PullRequest) {
|
||||
trusted, err := actions_service.GetPullRequestPosterIsTrustedWithActions(ctx, pull)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPullRequestUserIsTrustedWithActions", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["PullRequestPosterIsNotTrustedWithActions"] = trusted == actions_service.UserIsNotTrustedWithActions
|
||||
ctx.Data["PullRequestPosterIsExplicitlyTrustedWithActions"] = trusted == actions_service.UserIsExplicitlyTrustedWithActions
|
||||
ctx.Data["PullRequestPosterIsImplicitlyTrustedWithActions"] = trusted == actions_service.UserIsImplicitlyTrustedWithActions
|
||||
|
||||
someRunsNeedApproval, err := actions_model.HasRunThatNeedApproval(ctx, pull.Issue.RepoID, pull.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasRunThatNeedApproval", err)
|
||||
}
|
||||
ctx.Data["SomePullRequestRunsNeedApproval"] = someRunsNeedApproval
|
||||
|
||||
ctx.Data["UserCanDelegateTrustWithPullRequest"] = context.CheckRepoDelegateActionTrust(ctx)
|
||||
}
|
||||
|
||||
func UpdateTrustWithPullRequestActions(ctx *context.Context) {
|
||||
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.NotFound("GetPullRequestByIndex", err)
|
||||
} else {
|
||||
ctx.ServerError("GetPullRequestByIndex", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
trust := ctx.FormString("trust")
|
||||
|
||||
if err := actions_service.UpdateTrustedWithPullRequest(ctx, ctx.Doer.ID, pr, actions_service.TrustUpdate(trust)); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(fmt.Sprintf("%s#pull-request-trust-panel", pr.Issue.Link()))
|
||||
}
|
||||
|
||||
@@ -846,6 +846,7 @@ func registerRoutes(m *web.Route) {
|
||||
reqRepoProjectsWriter := context.RequireRepoWriter(unit.TypeProjects)
|
||||
reqRepoActionsReader := context.RequireRepoReader(unit.TypeActions)
|
||||
reqRepoActionsWriter := context.RequireRepoWriter(unit.TypeActions)
|
||||
reqRepoDelegateActionTrust := context.RequireRepoDelegateActionTrust()
|
||||
|
||||
reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
@@ -1215,6 +1216,7 @@ func registerRoutes(m *web.Route) {
|
||||
m.Group("/{type:issues|pulls}", func() {
|
||||
m.Group("/{index}", func() {
|
||||
m.Post("/title", repo.UpdateIssueTitle)
|
||||
m.Post("/action-user-trust", reqRepoActionsReader, actions.MustEnableActions, reqRepoDelegateActionTrust, repo.UpdateTrustWithPullRequestActions)
|
||||
m.Post("/content", repo.UpdateIssueContent)
|
||||
m.Post("/deadline", web.Bind(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline)
|
||||
m.Post("/watch", repo.IssueWatch)
|
||||
|
||||
@@ -111,6 +111,19 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func RequireRepoDelegateActionTrust() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
if CheckRepoDelegateActionTrust(ctx) {
|
||||
return
|
||||
}
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckRepoDelegateActionTrust(ctx *Context) bool {
|
||||
return ctx.Repo.IsAdmin() || (ctx.IsSigned && ctx.Doer.IsAdmin) || ctx.Repo.CanWrite(unit.TypeActions)
|
||||
}
|
||||
|
||||
// CheckRepoScopedToken check whether personal access token has repo scope
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
data-workflow-url="{{.WorkflowURL}}"
|
||||
data-initial-post-response="{{.InitialData}}"
|
||||
data-initial-artifacts-response="{{.InitialArtifactsData}}"
|
||||
data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}"
|
||||
data-locale-approve="{{ctx.Locale.Tr "repo.pulls.poster_manage_approval"}}"
|
||||
data-locale-cancel="{{ctx.Locale.Tr "cancel"}}"
|
||||
data-locale-rerun="{{ctx.Locale.Tr "rerun"}}"
|
||||
data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}"
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "repo/pulls/trust" .}}
|
||||
{{template "repo/issue/view_content/update_branch_by_merge" $}}
|
||||
{{else if .Issue.PullRequest.IsChecking}}
|
||||
<div class="item">
|
||||
@@ -187,6 +188,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{template "repo/pulls/trust" .}}
|
||||
{{template "repo/issue/view_content/update_branch_by_merge" $}}
|
||||
{{if .Issue.PullRequest.IsEmpty}}
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
{{/*
|
||||
Template Attributes:
|
||||
* CanReadUnitActions: true if the actions unit is active and readable
|
||||
* SomePullRequestRunsNeedApproval: true if there is at least one run waiting for approval
|
||||
* UserCanDelegateTrustWithPullRequest: true if the user can delegate trust in the context of pull requests
|
||||
* PullRequestPosterIsNotTrustedWithActions: true if the poster of the pull request needs to be approved to run actions
|
||||
* PullRequestPosterIsExplicitlyTrustedWithActions: true if the poster of the pull request is trusted to run actions (once or always)
|
||||
* PullRequestPosterIsImplicitlyTrustedWithActions: true if the poster of the pull request is trusted to run actions because of elevated permissions
|
||||
* Link: URL to the pull request
|
||||
*/}}
|
||||
|
||||
{{if .CanReadUnitActions}}
|
||||
{{if and .UserCanDelegateTrustWithPullRequest .PullRequestPosterIsExplicitlyTrustedWithActions}}
|
||||
<div class="pull-request-trust-panel" id="pull-request-trust-panel">
|
||||
<div class="divider"></div>
|
||||
<div class="item item-section">
|
||||
<div class="item-section-left flex-text-inline" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.poster_is_trusted.tooltip"}}">
|
||||
{{ctx.Locale.Tr "repo.pulls.poster_is_trusted" "https://forgejo.org/docs/latest/user/actions/security-pull-request/"}}
|
||||
</div>
|
||||
<div class="item-section-right">
|
||||
<form id="pull-request-trust-panel-revoke" class="ui form" method="post" action="{{.Link}}/action-user-trust">
|
||||
<input type="hidden" name="trust" value="revoke">
|
||||
<button class="ui primary button" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.poster_trust_revoke.tooltip"}}">{{ctx.Locale.Tr "repo.pulls.poster_trust_revoke"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if and .PullRequestPosterIsNotTrustedWithActions .SomePullRequestRunsNeedApproval}}
|
||||
<div class="pull-request-trust-panel" id="pull-request-trust-panel">
|
||||
<div class="divider"></div>
|
||||
<div class="item item-section">
|
||||
<div class="item-section-left flex-text-inline" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.poster_requires_approval.tooltip"}}">
|
||||
{{svg "octicon-alert" 16 "text red"}}
|
||||
{{ctx.Locale.Tr "repo.pulls.poster_requires_approval" "https://forgejo.org/docs/latest/user/actions/security-pull-request/"}}
|
||||
</div>
|
||||
{{if .UserCanDelegateTrustWithPullRequest}}
|
||||
<div class="item-section-right">
|
||||
<div class="tw-inline-block">
|
||||
<form id="pull-request-trust-panel-deny" class="ui form" method="post" action="{{.Link}}/action-user-trust">
|
||||
<input type="hidden" name="trust" value="deny">
|
||||
<button class="ui primary button" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.poster_trust_deny.tooltip"}}">{{ctx.Locale.Tr "repo.pulls.poster_trust_deny"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tw-inline-block">
|
||||
<form id="pull-request-trust-panel-once" class="ui form" method="post" action="{{.Link}}/action-user-trust">
|
||||
<input type="hidden" name="trust" value="once">
|
||||
<button class="ui primary button" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.poster_trust_once.tooltip"}}">{{ctx.Locale.Tr "repo.pulls.poster_trust_once"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tw-inline-block">
|
||||
<form id="pull-request-trust-panel-always" class="ui form" method="post" action="{{.Link}}/action-user-trust">
|
||||
<input type="hidden" name="trust" value="always">
|
||||
<button class="ui primary button" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.poster_trust_always.tooltip"}}">{{ctx.Locale.Tr "repo.pulls.poster_trust_always"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,355 @@
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
actions_module "forgejo.org/modules/actions"
|
||||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/structs"
|
||||
actions_service "forgejo.org/services/actions"
|
||||
pull_service "forgejo.org/services/pull"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
files_service "forgejo.org/services/repository/files"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func actionsTrustTestClickTrustPanel(t *testing.T, session *TestSession, url, trust string) {
|
||||
// an admin approves the run once
|
||||
req := NewRequest(t, "GET", url)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
htmlDoc.AssertElement(t, "#pull-request-trust-panel", true)
|
||||
link, exists := htmlDoc.doc.Find("#pull-request-trust-panel-" + trust).Attr("action")
|
||||
require.True(t, exists)
|
||||
actualTrust, exists := htmlDoc.doc.Find(fmt.Sprintf("#pull-request-trust-panel-%s input[name='trust']", trust)).Attr("value")
|
||||
require.True(t, exists)
|
||||
require.Equal(t, trust, actualTrust)
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"trust": trust,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func actionsTrustTestAssertTrustPanelPresence(t *testing.T, session *TestSession, url string, present bool) {
|
||||
t.Helper()
|
||||
req := NewRequest(t, "GET", url)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
htmlDoc.AssertElement(t, ".error-code", false)
|
||||
htmlDoc.AssertElement(t, "#pull-request-trust-panel", present)
|
||||
}
|
||||
|
||||
func actionsTrustTestAssertTrustPanel(t *testing.T, session *TestSession, url string) {
|
||||
t.Helper()
|
||||
actionsTrustTestAssertTrustPanelPresence(t, session, url, true)
|
||||
}
|
||||
|
||||
func actionsTrustTestAssertNoTrustPanel(t *testing.T, session *TestSession, url string) {
|
||||
t.Helper()
|
||||
actionsTrustTestAssertTrustPanelPresence(t, session, url, false)
|
||||
}
|
||||
|
||||
func actionsTrustTestCreateBaseRepo(t *testing.T, owner *user_model.User) (*repo_model.Repository, func()) {
|
||||
t.Helper()
|
||||
|
||||
// create the base repo
|
||||
baseRepo, _, f := tests.CreateDeclarativeRepo(t, owner, "repo-pull-request",
|
||||
[]unit_model.Type{unit_model.TypeActions}, nil, nil,
|
||||
)
|
||||
|
||||
// add workflow file to the base repo
|
||||
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, owner, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: ".forgejo/workflows/pr.yml",
|
||||
ContentReader: strings.NewReader(`
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo helloworld
|
||||
`),
|
||||
},
|
||||
},
|
||||
Message: "add workflow",
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
Author: &files_service.IdentityOptions{
|
||||
Name: owner.Name,
|
||||
Email: owner.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
Name: owner.Name,
|
||||
Email: owner.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, addWorkflowToBaseResp)
|
||||
return baseRepo, f
|
||||
}
|
||||
|
||||
func actionsTrustTestRequireRun(t *testing.T, repo *repo_model.Repository, modifiedFiles *structs.FilesResponse) {
|
||||
t.Helper()
|
||||
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, CommitSHA: modifiedFiles.Commit.SHA})
|
||||
require.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
|
||||
require.Equal(t, actions_model.StatusWaiting.String(), actionRun.Status.String())
|
||||
unittest.BeanExists(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: repo.ID})
|
||||
}
|
||||
|
||||
func actionsTrustTestRepoCreateBranch(t *testing.T, doer *user_model.User, repo *repo_model.Repository) *structs.FilesResponse {
|
||||
t.Helper()
|
||||
|
||||
return actionsTrustTestModifyRepo(t, doer, repo, "file_in_fork.txt", "main", "fork-branch-1")
|
||||
}
|
||||
|
||||
func actionsTrustTestRepoModify(t *testing.T, doer *user_model.User, baseRepo, headRepo *repo_model.Repository, filename string) *structs.FilesResponse {
|
||||
t.Helper()
|
||||
|
||||
modified := actionsTrustTestModifyRepo(t, doer, headRepo, filename, "fork-branch-1", "fork-branch-1")
|
||||
// the creation of the run is not synchronous
|
||||
require.Eventually(t, func() bool {
|
||||
return unittest.BeanExists(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modified.Commit.SHA})
|
||||
}, 60*time.Second, time.Millisecond*100)
|
||||
return modified
|
||||
}
|
||||
|
||||
func actionsTrustTestModifyRepo(t *testing.T, doer *user_model.User, repo *repo_model.Repository, filename, oldBranch, newBranch string) *structs.FilesResponse {
|
||||
t.Helper()
|
||||
|
||||
// add a new file to the forked repo
|
||||
addFile, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: filename,
|
||||
ContentReader: strings.NewReader("content"),
|
||||
},
|
||||
},
|
||||
Message: "add " + filename,
|
||||
OldBranch: oldBranch,
|
||||
NewBranch: newBranch,
|
||||
Author: &files_service.IdentityOptions{
|
||||
Name: doer.Name,
|
||||
Email: doer.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
Name: doer.Name,
|
||||
Email: doer.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, addFile)
|
||||
return addFile
|
||||
}
|
||||
|
||||
func actionsTrustTestCreatePullRequestFromForkedRepo(t *testing.T, baseUser *user_model.User, baseRepo *repo_model.Repository, headUser *user_model.User) (*repo_model.Repository, *issues_model.PullRequest, *structs.FilesResponse) {
|
||||
t.Helper()
|
||||
|
||||
forkRepo := func(t *testing.T, baseUser *user_model.User, baseRepo *repo_model.Repository, headUser *user_model.User) *repo_model.Repository {
|
||||
t.Helper()
|
||||
|
||||
// create the forked repo
|
||||
forkedRepo, err := repo_service.ForkRepositoryAndUpdates(git.DefaultContext, baseUser, headUser, repo_service.ForkRepoOptions{
|
||||
BaseRepo: baseRepo,
|
||||
Name: "forked-repo-pull-request",
|
||||
Description: "test pull-request event",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, forkedRepo)
|
||||
return forkedRepo
|
||||
}
|
||||
|
||||
forkedRepo := forkRepo(t, baseUser, baseRepo, headUser)
|
||||
addFileToForkedResp := actionsTrustTestRepoCreateBranch(t, headUser, forkedRepo)
|
||||
|
||||
// create Pull
|
||||
pullIssue := &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
Title: "Test pull-request",
|
||||
PosterID: headUser.ID,
|
||||
Poster: headUser,
|
||||
IsPull: true,
|
||||
}
|
||||
pullRequest := &issues_model.PullRequest{
|
||||
HeadRepoID: forkedRepo.ID,
|
||||
BaseRepoID: baseRepo.ID,
|
||||
HeadBranch: "fork-branch-1",
|
||||
BaseBranch: "main",
|
||||
HeadRepo: forkedRepo,
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
// create the pull request
|
||||
err := pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
actionsTrustTestRequireRun(t, baseRepo, addFileToForkedResp)
|
||||
|
||||
return forkedRepo, pullRequest, addFileToForkedResp
|
||||
}
|
||||
|
||||
func TestActionsPullRequestTrustPanel(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
ownerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo
|
||||
|
||||
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // a regular user with no specific permission
|
||||
regularSession := loginUser(t, regularUser.Name)
|
||||
|
||||
userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // the instance admin
|
||||
adminSession := loginUser(t, userAdmin.Name)
|
||||
|
||||
baseRepo, f := actionsTrustTestCreateBaseRepo(t, ownerUser)
|
||||
defer f()
|
||||
|
||||
forkedRepo, pullRequest, addFileToForkedResp := actionsTrustTestCreatePullRequestFromForkedRepo(t, ownerUser, baseRepo, regularUser)
|
||||
pullRequestLink := pullRequest.Issue.Link()
|
||||
|
||||
t.Run("Regular user sees a pending approval on a newly created pull request from a fork", func(t *testing.T) {
|
||||
actionsTrustTestAssertTrustPanel(t, regularSession, pullRequestLink)
|
||||
})
|
||||
|
||||
t.Run("Admin approves runs once", func(t *testing.T) {
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: addFileToForkedResp.Commit.SHA})
|
||||
|
||||
{
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: addFileToForkedResp.Commit.SHA})
|
||||
assert.True(t, actionRun.NeedApproval)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked.String(), actionRunJob.Status.String())
|
||||
}
|
||||
|
||||
actionsTrustTestAssertTrustPanel(t, adminSession, pullRequestLink)
|
||||
actionsTrustTestClickTrustPanel(t, adminSession, pullRequestLink, string(actions_service.UserTrustedOnce))
|
||||
|
||||
{
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), actionRunJob.Status.String())
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
||||
assert.False(t, actionRun.NeedApproval)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All users sees no pending approval because it was approved once", func(t *testing.T) {
|
||||
actionsTrustTestAssertNoTrustPanel(t, regularSession, pullRequestLink)
|
||||
actionsTrustTestAssertNoTrustPanel(t, adminSession, pullRequestLink)
|
||||
})
|
||||
|
||||
modifiedForkedResp := actionsTrustTestRepoModify(t, regularUser, baseRepo, forkedRepo, "add_file_one.txt")
|
||||
|
||||
t.Run("Regular user sees a pending approval on a modified pull request from a fork (2)", func(t *testing.T) {
|
||||
actionsTrustTestAssertTrustPanel(t, regularSession, pullRequestLink)
|
||||
})
|
||||
|
||||
t.Run("Admin denies runs", func(t *testing.T) {
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modifiedForkedResp.Commit.SHA})
|
||||
|
||||
{
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modifiedForkedResp.Commit.SHA})
|
||||
assert.True(t, actionRun.NeedApproval)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked.String(), actionRunJob.Status.String())
|
||||
}
|
||||
|
||||
actionsTrustTestAssertTrustPanel(t, adminSession, pullRequestLink)
|
||||
actionsTrustTestClickTrustPanel(t, adminSession, pullRequestLink, string(actions_service.UserTrustDenied))
|
||||
|
||||
{
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), actionRunJob.Status.String())
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
||||
assert.False(t, actionRun.NeedApproval)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All users sees no pending approval because it was denied", func(t *testing.T) {
|
||||
actionsTrustTestAssertNoTrustPanel(t, regularSession, pullRequestLink)
|
||||
actionsTrustTestAssertNoTrustPanel(t, adminSession, pullRequestLink)
|
||||
})
|
||||
|
||||
modifiedForkedResp = actionsTrustTestRepoModify(t, regularUser, baseRepo, forkedRepo, "add_file_two.txt")
|
||||
|
||||
t.Run("Regular user sees a pending approval on a modified pull request from a fork (2)", func(t *testing.T) {
|
||||
actionsTrustTestAssertTrustPanel(t, regularSession, pullRequestLink)
|
||||
})
|
||||
|
||||
t.Run("Admin always trusts the poster", func(t *testing.T) {
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modifiedForkedResp.Commit.SHA})
|
||||
|
||||
{
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modifiedForkedResp.Commit.SHA})
|
||||
assert.True(t, actionRun.NeedApproval)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked.String(), actionRunJob.Status.String())
|
||||
}
|
||||
|
||||
actionsTrustTestAssertTrustPanel(t, adminSession, pullRequestLink)
|
||||
actionsTrustTestClickTrustPanel(t, adminSession, pullRequestLink, string(actions_service.UserAlwaysTrusted))
|
||||
|
||||
{
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), actionRunJob.Status.String())
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
||||
assert.False(t, actionRun.NeedApproval)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Regular users sees no pending approval because it was approved", func(t *testing.T) {
|
||||
actionsTrustTestAssertNoTrustPanel(t, regularSession, pullRequestLink)
|
||||
})
|
||||
|
||||
modifiedForkedResp = actionsTrustTestRepoModify(t, regularUser, baseRepo, forkedRepo, "add_file_three.txt")
|
||||
|
||||
t.Run("No need for approval because the poster is always trusted", func(t *testing.T) {
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modifiedForkedResp.Commit.SHA})
|
||||
assert.False(t, actionRun.NeedApproval)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), actionRunJob.Status.String())
|
||||
})
|
||||
|
||||
t.Run("Admin revokes the trusted poster", func(t *testing.T) {
|
||||
actionsTrustTestAssertTrustPanel(t, adminSession, pullRequestLink)
|
||||
actionsTrustTestClickTrustPanel(t, adminSession, pullRequestLink, string(actions_service.UserTrustRevoked))
|
||||
})
|
||||
|
||||
modifiedForkedResp = actionsTrustTestRepoModify(t, regularUser, baseRepo, forkedRepo, "add_file_four.txt")
|
||||
|
||||
t.Run("There needs to be an approval again because the user is no longer trusted", func(t *testing.T) {
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: modifiedForkedResp.Commit.SHA})
|
||||
assert.True(t, actionRun.NeedApproval)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked.String(), actionRunJob.Status.String())
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -397,6 +397,44 @@ test('historical attempt dropdown interactions', async () => {
|
||||
expect(window.location.href).toEqual(toAbsoluteUrl('/user1/repo2/actions/runs/123/jobs/1/attempt/2'));
|
||||
});
|
||||
|
||||
test('run approval interaction', async () => {
|
||||
const pullRequestLink = '/example-org/example-repo/pulls/456';
|
||||
const wrapper = mount(RepoActionView, {
|
||||
props: {
|
||||
...defaultTestProps,
|
||||
initialJobData: {
|
||||
state: {
|
||||
run: {
|
||||
canApprove: true,
|
||||
status: 'waiting',
|
||||
commit: {
|
||||
pusher: {},
|
||||
branch: {
|
||||
link: toAbsoluteUrl(pullRequestLink),
|
||||
},
|
||||
},
|
||||
},
|
||||
currentJob: {
|
||||
steps: [
|
||||
{
|
||||
summary: 'Test Job',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
stepsLog: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
const approve = wrapper.findAll('button').filter((button) => button.text() === 'Locale Approve');
|
||||
expect(approve.length).toEqual(1);
|
||||
approve[0].trigger('click');
|
||||
expect(window.location.href).toEqual(toAbsoluteUrl(`${pullRequestLink}#pull-request-trust-panel`));
|
||||
});
|
||||
|
||||
test('artifacts download links', async () => {
|
||||
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
||||
vi.spyOn(global, 'fetch').mockImplementation((url, opts) => {
|
||||
|
||||
@@ -223,7 +223,8 @@ export default {
|
||||
},
|
||||
// approve a run
|
||||
approveRun() {
|
||||
POST(`${this.run.link}/approve`);
|
||||
const url = `${this.run.commit.branch.link}#pull-request-trust-panel`;
|
||||
window.location.href = url;
|
||||
},
|
||||
// show/hide the step logs for a group
|
||||
toggleGroupLogs(event) {
|
||||
|
||||
Reference in New Issue
Block a user