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:
Earl Warren
2025-11-06 11:07:39 +01:00
parent 465d057fae
commit 57f986c7b1
10 changed files with 559 additions and 2 deletions
+14
View File
@@ -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",
+71
View File
@@ -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()))
}
+2
View File
@@ -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)
+13
View File
@@ -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 {
+1 -1
View File
@@ -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>
+61
View File
@@ -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}}
+355
View File
@@ -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) => {
+2 -1
View File
@@ -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) {