Skip to content

Commit 97a374f

Browse files
zakiskpipelines-as-code[bot]
authored andcommitted
Add support for GitOps commands on pushed commits in GitLab
This adds support of GitOps commands on pushed commits in GitLab so that users can trigger PipelineRuns on pushed commit. below commands are supported: 1) [/test, /retest] 2) [/test, /retest] prName 3) [/test, /retest] branch:test 4) /cancel 5) /cancel prName 6) /cancel branch:test https://issues.redhat.com/browse/SRVKP-7105 Signed-off-by: Zaki Shaikh <[email protected]>
1 parent 9d8a5ab commit 97a374f

17 files changed

+776
-76
lines changed

docs/content/docs/guide/gitops_commands.md

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Please be aware that GitOps commands such as `/test` and others will not functio
3737

3838
## GitOps Commands on Pushed Commits
3939

40+
{{< support_matrix github_app="true" github_webhook="true" gitea="false" gitlab="true" bitbucket_cloud="false" bitbucket_server="false" >}}
41+
4042
If you want to trigger a GitOps command on a pushed commit, you can include the `GitOps` comments within your commit messages. These comments can be used to restart either all pipelines or specific ones. Here's how it works:
4143

4244
For restarting all pipeline runs:
@@ -73,6 +75,18 @@ This means:
7375
Please note that the `/ok-to-test` command does not work on pushed commits, as it is specifically intended for pull requests to manage authorization. Since only authorized users are allowed to send `GitOps` commands on pushed commits,
7476
there is no need to use the `ok-to-test` command in this context.
7577

78+
For example, when executing a GitOps command like `/test test-pr branch:test` on a pushed commit, verify that the `test-pr` is on the test branch in your repository and includes the `on-event` and `on-target-branch` annotations as demonstrated below:
79+
80+
```yaml
81+
kind: PipelineRun
82+
metadata:
83+
name: "test-pr"
84+
annotations:
85+
pipelinesascode.tekton.dev/on-target-branch: "[test]"
86+
pipelinesascode.tekton.dev/on-event: "[push]"
87+
spec:
88+
```
89+
7690
To issue a `GitOps` comment on a pushed commit, you can follow these steps:
7791

7892
1. Go to your repository.

pkg/matcher/annotation_matcher_test.go

+1-46
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,10 @@ import (
2121
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
2222
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
2323
ghprovider "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github"
24-
glprovider "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/gitlab"
25-
gltesthelper "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/gitlab/test"
2624
testclient "github.com/openshift-pipelines/pipelines-as-code/pkg/test/clients"
2725
ghtesthelper "github.com/openshift-pipelines/pipelines-as-code/pkg/test/github"
2826
testnewrepo "github.com/openshift-pipelines/pipelines-as-code/pkg/test/repository"
2927
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
30-
gitlab "gitlab.com/gitlab-org/api/client-go"
3128
"go.uber.org/zap"
3229
zapobserver "go.uber.org/zap/zaptest/observer"
3330
"gotest.tools/v3/assert"
@@ -1398,53 +1395,11 @@ func TestMatchPipelinerunAnnotationAndRepositories(t *testing.T) {
13981395
ghCs, _ := testclient.SeedTestData(t, ctx, tt.args.data)
13991396
runTest(ctx, t, tt, vcx, ghCs)
14001397

1401-
glFakeClient, glMux, glTeardown := gltesthelper.Setup(t)
1402-
defer glTeardown()
1403-
glVcx := &glprovider.Provider{
1404-
Client: glFakeClient,
1405-
Token: github.Ptr("None"),
1406-
}
1407-
if len(tt.args.fileChanged) > 0 {
1408-
commitFiles := []*gitlab.MergeRequestDiff{}
1409-
pushFileChanges := []*gitlab.Diff{}
1410-
if tt.args.runevent.TriggerTarget == "push" {
1411-
for _, v := range tt.args.fileChanged {
1412-
pushFileChanges = append(pushFileChanges, &gitlab.Diff{
1413-
NewPath: v.FileName,
1414-
RenamedFile: v.RenamedFile,
1415-
DeletedFile: v.DeletedFile,
1416-
NewFile: v.NewFile,
1417-
})
1418-
}
1419-
glMux.HandleFunc(fmt.Sprintf("/projects/0/repository/commits/%s/diff",
1420-
tt.args.runevent.SHA), func(rw http.ResponseWriter, _ *http.Request) {
1421-
jeez, err := json.Marshal(pushFileChanges)
1422-
assert.NilError(t, err)
1423-
_, _ = rw.Write(jeez)
1424-
})
1425-
} else {
1426-
for _, v := range tt.args.fileChanged {
1427-
commitFiles = append(commitFiles, &gitlab.MergeRequestDiff{
1428-
NewPath: v.FileName,
1429-
RenamedFile: v.RenamedFile,
1430-
DeletedFile: v.DeletedFile,
1431-
NewFile: v.NewFile,
1432-
})
1433-
}
1434-
url := fmt.Sprintf("/projects/0/merge_requests/%d/diffs", tt.args.runevent.PullRequestNumber)
1435-
glMux.HandleFunc(url, func(w http.ResponseWriter, _ *http.Request) {
1436-
jeez, err := json.Marshal(commitFiles)
1437-
assert.NilError(t, err)
1438-
_, _ = w.Write(jeez)
1439-
})
1440-
}
1441-
}
1442-
14431398
tt.args.runevent.Provider = &info.Provider{
14441399
Token: "NONE",
14451400
}
14461401

1447-
runTest(ctx, t, tt, glVcx, ghCs)
1402+
runTest(ctx, t, tt, vcx, ghCs)
14481403
})
14491404
}
14501405
}

pkg/pipelineascode/secret.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type SecretFromRepository struct {
2929
Logger *zap.SugaredLogger
3030
}
3131

32-
// SecretFromRepository grab the secret from the repository CRD.
32+
// Get grab the secret from the repository CRD.
3333
func (s *SecretFromRepository) Get(ctx context.Context) error {
3434
var err error
3535
if s.Repo.Spec.GitProvider == nil {

pkg/provider/github/github.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,8 @@ func uniqueRepositoryID(repoIDs []int64, id int64) []int64 {
586586
return r
587587
}
588588

589-
// isBranchContainsCommit checks whether provided branch has sha or not.
590-
func (v *Provider) isBranchContainsCommit(ctx context.Context, runevent *info.Event, branchName string) error {
589+
// isHeadCommitOfBranch checks whether provided branch is valid or not and SHA is HEAD commit of the branch.
590+
func (v *Provider) isHeadCommitOfBranch(ctx context.Context, runevent *info.Event, branchName string) error {
591591
if v.Client == nil {
592592
return fmt.Errorf("no github client has been initialized, " +
593593
"exiting... (hint: did you forget setting a secret on your repo?)")

pkg/provider/github/github_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,7 @@ func TestCreateToken(t *testing.T) {
10871087
}
10881088
}
10891089

1090-
func TestGetBranch(t *testing.T) {
1090+
func TestIsHeadCommitOfBranch(t *testing.T) {
10911091
tests := []struct {
10921092
name string
10931093
sha string
@@ -1125,7 +1125,7 @@ func TestGetBranch(t *testing.T) {
11251125

11261126
ctx, _ := rtesting.SetupFakeContext(t)
11271127
provider := &Provider{Client: fakeclient}
1128-
err := provider.isBranchContainsCommit(ctx, runEvent, "test1")
1128+
err := provider.isHeadCommitOfBranch(ctx, runEvent, "test1")
11291129
assert.Equal(t, err != nil, tt.wantErr)
11301130
})
11311131
}

pkg/provider/github/parse_payload.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C
466466
}
467467

468468
// Check if the specified branch contains the commit
469-
if err = v.isBranchContainsCommit(ctx, runevent, branchName); err != nil {
469+
if err = v.isHeadCommitOfBranch(ctx, runevent, branchName); err != nil {
470470
if provider.IsCancelComment(event.GetComment().GetBody()) {
471471
runevent.CancelPipelineRuns = false
472472
}
@@ -476,6 +476,6 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C
476476
runevent.HeadBranch = branchName
477477
runevent.BaseBranch = branchName
478478

479-
v.Logger.Infof("commit_comment: pipelinerun %s on %s/%s#%s has been requested", action, runevent.Organization, runevent.Repository, runevent.SHA)
479+
v.Logger.Infof("github commit_comment: pipelinerun %s on %s/%s#%s has been requested", action, runevent.Organization, runevent.Repository, runevent.SHA)
480480
return runevent, nil
481481
}

pkg/provider/github/parse_payload_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ func TestParsePayLoad(t *testing.T) {
530530
isCancelPipelineRunEnabled: true,
531531
},
532532
{
533-
name: "good/commit comment for cancel a pr with invalid branch name",
533+
name: "bad/commit comment for cancel a pr with invalid branch name",
534534
eventType: "commit_comment",
535535
triggerTarget: "push",
536536
githubClient: true,
@@ -550,7 +550,7 @@ func TestParsePayLoad(t *testing.T) {
550550
wantErrString: "404 Not Found",
551551
},
552552
{
553-
name: "commit comment to retest a pr with a SHA that does not exist in the main branch",
553+
name: "commit comment to retest a pr with a SHA is not HEAD commit of the main branch",
554554
eventType: "commit_comment",
555555
triggerTarget: "push",
556556
githubClient: true,

pkg/provider/gitlab/detect.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import (
1010
"go.uber.org/zap"
1111
)
1212

13-
// Detect processes event and detect if it is a gitlab event, whether to process or reject it
14-
// returns (if is a GL event, whether to process or reject, logger with event metadata,, error if any occurred).
13+
// Detect detects events and validates if it is a valid gitlab event Pipelines as Code supports and
14+
// decides whether to process or reject it.
15+
// returns a boolean value whether to process or reject, logger with event metadata, and error if any occurred.
1516
func (v *Provider) Detect(req *http.Request, payload string, logger *zap.SugaredLogger) (bool, bool, *zap.SugaredLogger, string, error) {
1617
isGL := false
1718
event := req.Header.Get("X-Gitlab-Event")
@@ -58,6 +59,19 @@ func (v *Provider) Detect(req *http.Request, payload string, logger *zap.Sugared
5859
return setLoggerAndProceed(true, "", nil)
5960
}
6061
return setLoggerAndProceed(false, "comments on closed merge requests is not supported", nil)
62+
case *gitlab.CommitCommentEvent:
63+
comment := gitEvent.ObjectAttributes.Note
64+
if gitEvent.ObjectAttributes.Action == gitlab.CommentEventActionCreate {
65+
if provider.IsTestRetestComment(comment) || provider.IsCancelComment(comment) {
66+
return setLoggerAndProceed(true, "", nil)
67+
}
68+
// truncate comment to make logs readable
69+
if len(comment) > 50 {
70+
comment = comment[:50] + "..."
71+
}
72+
return setLoggerAndProceed(false, fmt.Sprintf("gitlab: commit_comment: unsupported GitOps comment \"%s\" on pushed commits", comment), nil)
73+
}
74+
return setLoggerAndProceed(false, fmt.Sprintf("gitlab: commit_comment: unsupported action \"%s\" with comment \"%s\"", gitEvent.ObjectAttributes.Action, comment), nil)
6175
default:
6276
return setLoggerAndProceed(false, "", fmt.Errorf("gitlab: event \"%s\" is not supported", event))
6377
}

pkg/provider/gitlab/detect_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"gotest.tools/v3/assert"
1212
)
1313

14+
const largeComment = "/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"
15+
1416
func TestProvider_Detect(t *testing.T) {
1517
sample := thelp.TEvent{
1618
Username: "foo",
@@ -126,6 +128,51 @@ func TestProvider_Detect(t *testing.T) {
126128
isGL: true,
127129
processReq: true,
128130
},
131+
{
132+
name: "bad/commit comment unsupported action",
133+
event: sample.CommitNoteEventAsJSON("/test", "update", "null"),
134+
eventType: gitlab.EventTypeNote,
135+
isGL: true,
136+
processReq: false,
137+
wantReason: "gitlab: commit_comment: unsupported action \"update\" with comment \"/test\"",
138+
},
139+
{
140+
name: "bad/commit comment unsupported gitops command",
141+
event: sample.CommitNoteEventAsJSON("/merge", "create", "null"),
142+
eventType: gitlab.EventTypeNote,
143+
isGL: true,
144+
processReq: false,
145+
wantReason: "gitlab: commit_comment: unsupported GitOps comment \"/merge\"",
146+
},
147+
{
148+
name: "bad/commit comment unsupported large comment",
149+
event: sample.CommitNoteEventAsJSON(largeComment, "create", "null"),
150+
eventType: gitlab.EventTypeNote,
151+
isGL: true,
152+
processReq: false,
153+
wantReason: "gitlab: commit_comment: unsupported GitOps comment \"/Lorem Ipsum is simply dummy text of the printing ...\"",
154+
},
155+
{
156+
name: "good/commit comment /test command",
157+
event: sample.CommitNoteEventAsJSON("/test", "create", "null"),
158+
eventType: gitlab.EventTypeNote,
159+
isGL: true,
160+
processReq: true,
161+
},
162+
{
163+
name: "good/commit comment /retest command",
164+
event: sample.CommitNoteEventAsJSON("/retest", "create", "null"),
165+
eventType: gitlab.EventTypeNote,
166+
isGL: true,
167+
processReq: true,
168+
},
169+
{
170+
name: "good/commit comment /cancel command",
171+
event: sample.CommitNoteEventAsJSON("/cancel", "create", "null"),
172+
eventType: gitlab.EventTypeNote,
173+
isGL: true,
174+
processReq: true,
175+
},
129176
}
130177

131178
for _, tt := range tests {

pkg/provider/gitlab/gitlab.go

+23-3
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ func (v *Provider) SetClient(_ context.Context, run *params.Run, runevent *info.
142142
// repository, runevent.SourceProjectID will not be 0 when SetClient is called from the pac-watcher code.
143143
// This is because, in the controller, SourceProjectID is set in the annotation of the pull request,
144144
// and runevent.SourceProjectID is set before SetClient is called. Therefore, we need to take
145-
// the ID from runevent.SourceProjectID.
146-
if runevent.SourceProjectID > 0 {
145+
// the ID from runevent.SourceProjectID when v.sourceProject is 0 (nil).
146+
if v.sourceProjectID == 0 && runevent.SourceProjectID > 0 {
147147
v.sourceProjectID = runevent.SourceProjectID
148148
}
149149

@@ -228,7 +228,9 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusOpts
228228
}
229229

230230
eventType := triggertype.IsPullRequestType(event.EventType)
231-
if opscomments.IsAnyOpsEventType(eventType.String()) {
231+
// When a GitOps command is sent on a pushed commit, it mistakenly treats it as a pull_request
232+
// and attempts to create a note, but notes are not intended for pushed commits.
233+
if event.TriggerTarget == triggertype.PullRequest && opscomments.IsAnyOpsEventType(event.EventType) {
232234
eventType = triggertype.PullRequest
233235
}
234236
// only add a note when we are on a MR
@@ -440,3 +442,21 @@ func (v *Provider) GetFiles(_ context.Context, runevent *info.Event) (changedfil
440442
func (v *Provider) CreateToken(_ context.Context, _ []string, _ *info.Event) (string, error) {
441443
return "", nil
442444
}
445+
446+
// isHeadCommitOfBranch validates that branch exists and the SHA is HEAD commit of the branch.
447+
func (v *Provider) isHeadCommitOfBranch(runevent *info.Event, branchName string) error {
448+
if v.Client == nil {
449+
return fmt.Errorf("no gitlab client has been initialized, " +
450+
"exiting... (hint: did you forget setting a secret on your repo?)")
451+
}
452+
branch, _, err := v.Client.Branches.GetBranch(v.sourceProjectID, branchName)
453+
if err != nil {
454+
return err
455+
}
456+
457+
if branch.Commit.ID == runevent.SHA {
458+
return nil
459+
}
460+
461+
return fmt.Errorf("provided SHA %s is not the HEAD commit of the branch %s", runevent.SHA, branchName)
462+
}

0 commit comments

Comments
 (0)