Skip to content

Commit b42d36d

Browse files
chmouelGemini
andcommitted
feat: Honor gitops in GitLab threads replies
Previously, the GitLab provider only checked the top-level comment of a discussion for the `/ok-to-test` command. This limited where the command could be effectively used. The `/ok-to-test` command is now recognized when posted as a reply within a merge request discussion thread. An E2E test was added to verify this behavior, and the `README.md` was updated to reflect the new capability. Jira: https://issues.redhat.com/browse/SRVKP-8324 Co-authored-by: Gemini <gemini@google.com> Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent 6a0184b commit b42d36d

File tree

5 files changed

+161
-16
lines changed

5 files changed

+161
-16
lines changed

pkg/provider/gitlab/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
3333
### Until there is a ask for it
3434
35-
- /ok-to-test in threads comments (only top level comment is supported atm).
3635
- caching API calls for permissions.
3736
3837
## NOTES

pkg/provider/gitlab/acl.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,22 @@ func (v *Provider) checkOkToTestCommentFromApprovedMember(ctx context.Context, e
4747
nextPage = resp.NextPage
4848
}
4949

50-
for _, comment := range discussions {
51-
// TODO: maybe we do threads in the future but for now we just check the top thread for ops related comments
52-
topthread := comment.Notes[0]
53-
if acl.MatchRegexp(acl.OKToTestCommentRegexp, topthread.Body) {
54-
commenterEvent := info.NewEvent()
55-
commenterEvent.Event = event.Event
56-
commenterEvent.Sender = topthread.Author.Username
57-
commenterEvent.BaseBranch = event.BaseBranch
58-
commenterEvent.HeadBranch = event.HeadBranch
59-
commenterEvent.DefaultBranch = event.DefaultBranch
60-
// TODO: we could probably do with caching when checking all issues?
61-
if v.checkMembership(ctx, commenterEvent, topthread.Author.ID) {
62-
return true, nil
50+
for _, discussion := range discussions {
51+
// Iterate through every note in the discussion thread and evaluate them.
52+
// If a note contains an OK-to-test command, verify the commenter's permission
53+
// (either project membership or presence in OWNERS/OWNERS_ALIASES).
54+
for _, note := range discussion.Notes {
55+
if acl.MatchRegexp(acl.OKToTestCommentRegexp, note.Body) {
56+
commenterEvent := info.NewEvent()
57+
commenterEvent.Event = event.Event
58+
commenterEvent.Sender = note.Author.Username
59+
commenterEvent.BaseBranch = event.BaseBranch
60+
commenterEvent.HeadBranch = event.HeadBranch
61+
commenterEvent.DefaultBranch = event.DefaultBranch
62+
// We could add caching for membership checks in the future.
63+
if v.checkMembership(ctx, commenterEvent, note.Author.ID) {
64+
return true, nil
65+
}
6366
}
6467
}
6568
}

pkg/provider/gitlab/acl_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestIsAllowed(t *testing.T) {
2929
commentContent string
3030
commentAuthor string
3131
commentAuthorID int
32+
threadFirstNote string
3233
}{
3334
{
3435
name: "check client has been set",
@@ -76,6 +77,23 @@ func TestIsAllowed(t *testing.T) {
7677
commentAuthor: "admin",
7778
commentAuthorID: 1111,
7879
},
80+
{
81+
name: "allowed when /ok-to-test is in a reply note",
82+
allowed: true,
83+
wantClient: true,
84+
fields: fields{
85+
userID: 6666,
86+
targetProjectID: 2525,
87+
},
88+
args: args{
89+
event: &info.Event{Sender: "noowner", PullRequestNumber: 2},
90+
},
91+
allowMemberID: 1111,
92+
threadFirstNote: "random comment",
93+
commentContent: "/ok-to-test",
94+
commentAuthor: "admin",
95+
commentAuthorID: 1111,
96+
},
7997
{
8098
name: "disallowed from non authorized note",
8199
wantClient: true,
@@ -111,8 +129,15 @@ func TestIsAllowed(t *testing.T) {
111129
thelp.MuxGetFile(mux, tt.fields.targetProjectID, "OWNERS", tt.ownerFile, false)
112130
}
113131
if tt.commentContent != "" {
114-
thelp.MuxDiscussionsNote(mux, tt.fields.targetProjectID,
115-
tt.args.event.PullRequestNumber, tt.commentAuthor, tt.commentAuthorID, tt.commentContent)
132+
if tt.threadFirstNote != "" {
133+
thelp.MuxDiscussionsNoteWithReply(mux, tt.fields.targetProjectID,
134+
tt.args.event.PullRequestNumber,
135+
"someuser", 2222, tt.threadFirstNote,
136+
tt.commentAuthor, tt.commentAuthorID, tt.commentContent)
137+
} else {
138+
thelp.MuxDiscussionsNote(mux, tt.fields.targetProjectID,
139+
tt.args.event.PullRequestNumber, tt.commentAuthor, tt.commentAuthorID, tt.commentContent)
140+
}
116141
} else {
117142
thelp.MuxDiscussionsNoteEmpty(mux, tt.fields.targetProjectID, tt.args.event.PullRequestNumber)
118143
}

pkg/provider/gitlab/test/test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,29 @@ func MuxDiscussionsNote(mux *http.ServeMux, pid, mrID int, author string, author
124124
})
125125
}
126126

127+
// MuxDiscussionsNoteWithReply returns a single discussion where the first note
128+
// does not contain the trigger but a subsequent reply does. This simulates
129+
// /ok-to-test being posted in a thread reply.
130+
func MuxDiscussionsNoteWithReply(mux *http.ServeMux, pid, mrID int, firstAuthor string, firstAuthorID int, firstContent, okAuthor string, okAuthorID int, okContent string) {
131+
path := fmt.Sprintf("/projects/%d/merge_requests/%d/discussions", pid, mrID)
132+
mux.HandleFunc(path, func(rw http.ResponseWriter, _ *http.Request) {
133+
fmt.Fprintf(rw, `[
134+
{
135+
"notes": [
136+
{
137+
"body": %q,
138+
"author": {"username": %q, "id": %d}
139+
},
140+
{
141+
"body": %q,
142+
"author": {"username": %q, "id": %d}
143+
}
144+
]
145+
}
146+
]`, firstContent, firstAuthor, firstAuthorID, okContent, okAuthor, okAuthorID)
147+
})
148+
}
149+
127150
func MuxGetFile(mux *http.ServeMux, pid int, fname, content string, wantErr bool) {
128151
mux.HandleFunc(fmt.Sprintf("/projects/%d/repository/files/%s/raw", pid, fname), func(rw http.ResponseWriter, _ *http.Request) {
129152
if wantErr {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//go:build e2e
2+
// +build e2e
3+
4+
package test
5+
6+
import (
7+
"context"
8+
"net/http"
9+
"testing"
10+
11+
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/cctx"
12+
tgitlab "github.com/openshift-pipelines/pipelines-as-code/test/pkg/gitlab"
13+
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/payload"
14+
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/scm"
15+
twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait"
16+
"github.com/tektoncd/pipeline/pkg/names"
17+
clientGitlab "gitlab.com/gitlab-org/api/client-go"
18+
"gotest.tools/v3/assert"
19+
)
20+
21+
// TestGitlabOpsCommentInThreadReply verifies that a /test command placed
22+
// in a reply within a discussion thread on a Merge Request is honored.
23+
func TestGitlabOpsCommentInThreadReply(t *testing.T) {
24+
targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
25+
ctx := context.Background()
26+
runcnx, opts, glprovider, err := tgitlab.Setup(ctx)
27+
assert.NilError(t, err)
28+
ctx, err = cctx.GetControllerCtxInfo(ctx, runcnx)
29+
assert.NilError(t, err)
30+
runcnx.Clients.Log.Info("Testing GitLab /test <PipelineRunName> in thread replies")
31+
32+
projectinfo, resp, err := glprovider.Client().Projects.GetProject(opts.ProjectID, nil)
33+
assert.NilError(t, err)
34+
if resp != nil && resp.StatusCode == http.StatusNotFound {
35+
t.Errorf("Repository %s not found in %s", opts.Organization, opts.Repo)
36+
}
37+
38+
// Create Repository CRD
39+
err = tgitlab.CreateCRD(ctx, projectinfo, runcnx, opts, targetNS, nil)
40+
assert.NilError(t, err)
41+
42+
// Create a basic PipelineRun
43+
entries, err := payload.GetEntries(map[string]string{
44+
".tekton/pipelinerun.yaml": "testdata/pipelinerun.yaml",
45+
}, targetNS, projectinfo.DefaultBranch, "pull_request", map[string]string{})
46+
assert.NilError(t, err)
47+
48+
// Create a branch with files and open a merge request
49+
targetRefName := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-test")
50+
gitCloneURL, err := scm.MakeGitCloneURL(projectinfo.WebURL, opts.UserName, opts.Password)
51+
assert.NilError(t, err)
52+
commitTitle := "Committing files from test on " + targetRefName
53+
scmOpts := &scm.Opts{
54+
GitURL: gitCloneURL,
55+
CommitTitle: commitTitle,
56+
Log: runcnx.Clients.Log,
57+
WebURL: projectinfo.WebURL,
58+
TargetRefName: targetRefName,
59+
BaseRefName: projectinfo.DefaultBranch,
60+
}
61+
_ = scm.PushFilesToRefGit(t, scmOpts, entries)
62+
mrTitle := "TestMergeRequest - " + targetRefName
63+
mrID, err := tgitlab.CreateMR(glprovider.Client(), opts.ProjectID, targetRefName, projectinfo.DefaultBranch, mrTitle)
64+
assert.NilError(t, err)
65+
defer tgitlab.TearDown(ctx, t, runcnx, glprovider, mrID, targetRefName, targetNS, opts.ProjectID)
66+
67+
// Create a discussion thread with an initial note
68+
disc, _, err := glprovider.Client().Discussions.CreateMergeRequestDiscussion(opts.ProjectID, mrID, &clientGitlab.CreateMergeRequestDiscussionOptions{
69+
Body: clientGitlab.Ptr("random initial note"),
70+
})
71+
assert.NilError(t, err)
72+
73+
// Wait for repository status to reflect a successful run triggered by the comment
74+
waitOpts := twait.Opts{
75+
RepoName: targetNS,
76+
Namespace: targetNS,
77+
MinNumberStatus: 1,
78+
PollTimeout: twait.DefaultTimeout,
79+
TargetSHA: "",
80+
}
81+
_, err = twait.UntilRepositoryUpdated(ctx, runcnx.Clients, waitOpts)
82+
assert.NilError(t, err)
83+
84+
runcnx.Clients.Log.Info("Updating discussion with /test comment in a reply thread")
85+
// Add a reply to the discussion containing /test
86+
_, _, err = glprovider.Client().Discussions.AddMergeRequestDiscussionNote(opts.ProjectID, mrID, disc.ID, &clientGitlab.AddMergeRequestDiscussionNoteOptions{
87+
Body: clientGitlab.Ptr("/test"),
88+
})
89+
assert.NilError(t, err)
90+
waitOpts.MinNumberStatus = 2
91+
_, err = twait.UntilRepositoryUpdated(ctx, runcnx.Clients, waitOpts)
92+
assert.NilError(t, err)
93+
94+
runcnx.Clients.Log.Info("Repository status updated after /test comment")
95+
}

0 commit comments

Comments
 (0)