fix: display the actions trust management panel on merged and closed pull requests (#12704)

It is possible for a user that is not trusted to run Forgejo Actions workflows on a repository to act on a pull request and trigger a workflow after it is merged or closed. For instance by modifying the title of the pull request or setting a label.

Closes forgejo/forgejo#12576

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

(can be removed for JavaScript changes)

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- User Interface bug fixes
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/12704): <!--number 12704 --><!--line 0 --><!--description ZGlzcGxheSB0aGUgYWN0aW9ucyB0cnVzdCBtYW5hZ2VtZW50IHBhbmVsIG9uIG1lcmdlZCBhbmQgY2xvc2VkIHB1bGwgcmVxdWVzdHM=-->display the actions trust management panel on merged and closed pull requests<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12704
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
(cherry picked from commit 5c61808014)
This commit is contained in:
limiting-factor 2026-05-23 18:21:21 +02:00 committed by forgejo-backport-action
commit 8863f097d2
3 changed files with 98 additions and 1 deletions

View file

@ -532,6 +532,11 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
}
}
PrepareViewPullInfoActions(ctx, pull)
if ctx.Written() {
return nil
}
return compareInfo
}

View file

@ -49,6 +49,7 @@
</div>
</div>
{{end}}
{{template "repo/pulls/trust" .}}
{{else if .Issue.IsClosed}}
<div class="item item-section text tw-flex-1">
<div>
@ -67,6 +68,7 @@
</div>
{{end}}
</div>
{{template "repo/pulls/trust" .}}
{{else if .IsPullFilesConflicted}}
<div class="item">
{{svg "octicon-x"}}

View file

@ -113,7 +113,7 @@ func actionsTrustTestCreateBaseRepo(t *testing.T, owner *user_model.User) (*repo
ContentReader: strings.NewReader(`
on:
pull_request:
types: [edited, opened, synchronize, reopened]
jobs:
test:
runs-on: docker
@ -271,6 +271,19 @@ func actionsTrustTestSetPullRequestWIP(t *testing.T, pullRequest *issues_model.P
require.NoError(t, pullRequest.LoadIssue(t.Context()))
}
func actionsTrustTestModifyTitlePullRequest(t *testing.T, token string, pullRequest *issues_model.PullRequest, sha string) {
t.Helper()
prAPILink := pullRequest.Issue.APIURL(t.Context())
req := NewRequestWithJSON(t, "PATCH", prAPILink, map[string]string{"title": "modified title"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: pullRequest.BaseRepoID, CommitSHA: sha}, "need_approval = true")
assert.True(t, actionRun.NeedApproval)
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: pullRequest.BaseRepoID})
assert.Equal(t, actions_model.StatusBlocked, actionRunJob.Status)
}
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
@ -438,6 +451,83 @@ func TestActionsPullRequestTrustPanelWIPConflicts(t *testing.T) {
})
}
func actionsTrustTestMergePullRequest(t *testing.T, token string, pullRequest *issues_model.PullRequest) {
t.Helper()
mergeURL := fmt.Sprintf("%s/pulls/%d/merge", pullRequest.Issue.Repo.APIURL(), pullRequest.Issue.Index)
req := NewRequestWithJSON(t, "POST", mergeURL, map[string]string{"do": "merge"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pullRequest.ID})
assert.True(t, pr.HasMerged)
}
func TestActionsPullRequestTrustPanelMergedOrClosed(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)
regularToken := getTokenForLoggedInUser(t, regularSession, auth_model.AccessTokenScopeAll)
userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // the instance admin
adminSession := loginUser(t, userAdmin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
baseRepo, f := actionsTrustTestCreateBaseRepo(t, ownerUser)
defer f()
_, pullRequest, addFileToForkedResp := actionsTrustTestCreatePullRequestFromForkedRepo(t, ownerUser, baseRepo, regularUser)
pullRequestLink := pullRequest.Issue.Link()
actionsTrustTestMergePullRequest(t, adminToken, pullRequest)
actionsTrustTestModifyTitlePullRequest(t, regularToken, pullRequest, addFileToForkedResp.Commit.SHA)
t.Run("Regular user sees pending approval even though PR is a merged PR", func(t *testing.T) {
actionsTrustTestAssertTrustPanel(t, regularSession, pullRequestLink)
})
})
}
func actionsTrustTestClosePullRequest(t *testing.T, token string, pullRequest *issues_model.PullRequest) {
t.Helper()
prAPILink := pullRequest.Issue.APIURL(t.Context())
req := NewRequestWithJSON(t, "PATCH", prAPILink, map[string]string{"state": "closed"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pullRequest.ID})
require.NoError(t, pr.LoadIssue(t.Context()))
assert.True(t, pr.Issue.IsClosed)
}
func TestActionsPullRequestTrustPanelClosed(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)
regularToken := getTokenForLoggedInUser(t, regularSession, auth_model.AccessTokenScopeAll)
userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // the instance admin
adminSession := loginUser(t, userAdmin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
baseRepo, f := actionsTrustTestCreateBaseRepo(t, ownerUser)
defer f()
_, pullRequest, addFileToForkedResp := actionsTrustTestCreatePullRequestFromForkedRepo(t, ownerUser, baseRepo, regularUser)
pullRequestLink := pullRequest.Issue.Link()
actionsTrustTestClosePullRequest(t, adminToken, pullRequest)
actionsTrustTestModifyTitlePullRequest(t, regularToken, pullRequest, addFileToForkedResp.Commit.SHA)
t.Run("Regular user sees pending approval even though PR is a closed PR", func(t *testing.T) {
actionsTrustTestAssertTrustPanel(t, regularSession, pullRequestLink)
})
})
}
func TestActionsPullRequestTrustCancelOnClose(t *testing.T) {
onApplicationRun(t, func(t *testing.T, u *url.URL) {
ownerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})