Skip to content

Commit 8b9dcb3

Browse files
committed
fix: Improve GitLab commit status handling
This commit addresses confusion around when GitLab comments are posted versus when commit status updates are used. Changes: * Documentation: Added clear explanation that comments are only posted when commit status updates fail, with visual example showing GitLab Pipelines tab where status appears when successful * Code clarity: Refactored commit status logic with better error handling and detailed logging when both source/target project status updates fail * Test reliability: Enhanced GitLab MR test to properly verify pipeline status success and ensure no comments are posted when status updates work * Test accuracy: Removed redundant CreateStatus test cases and improved SHA tracking for more reliable test execution The key fix is clarifying that Pipelines-as-Code tries to set commit status on both source (fork) and target (upstream) projects, and only falls back to posting comments when both attempts fail due to permission issues. Fixes the misleading documentation that didn't properly explain the relationship between GitLab API permissions, commit status updates, and comment posting behavior. Jira: https://issues.redhat.com/browse/SRVKP-8908 Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent 4e56944 commit 8b9dcb3

File tree

6 files changed

+193
-59
lines changed

6 files changed

+193
-59
lines changed

docs/content/docs/guide/repositorycrd.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: Repository CR
33
weight: 1
44
---
5+
56
# Repository CR
67

78
The Repository CR serves the following purposes:
@@ -127,7 +128,24 @@ access to the infrastructure.
127128

128129
## Disabling all comments for PipelineRuns on GitLab MR
129130

130-
`comment_strategy` allows you to disable the comments on GitLab MR for a Repository
131+
By default, Pipelines-as-Code attempts to update the commit status through the
132+
GitLab API, trying first on the source project (fork) and then falling back to
133+
the target project (upstream repository). If either status update succeeds, no
134+
comment is posted on the Merge Request.
135+
136+
When a status update succeeds, you can see the status in the GitLab UI in the `Pipelines` tab, as
137+
shown in the following example:
138+
139+
![Gitlab Pipelines from Pipelines-as-Code](/images/gitlab-pipelines-tab.png)
140+
141+
Comments are only posted when:
142+
143+
- Both commit status updates fail (typically due to insufficient token permissions)
144+
- The event type and repository settings allow commenting
145+
- The `comment_strategy` is not set to `disable_all`
146+
147+
To explicitly disable all comments on GitLab Merge Requests for a Repository,
148+
use the `comment_strategy` setting:
131149

132150
```yaml
133151
spec:
@@ -136,8 +154,6 @@ spec:
136154
comment_strategy: "disable_all"
137155
```
138156

139-
When you set the value of `comment_strategy` to `disable_all` it will not add any comment on the merge request for the start and the end of PipelineRun
140-
141157
## Disabling all comments for PipelineRuns in GitHub Pull Requests on GitHub Webhook Setup
142158

143159
`comment_strategy` allows you to disable the comments on GitHub PR for a Repository
492 KB
Loading

pkg/provider/gitlab/gitlab.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,17 +280,27 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusOpts
280280
Context: gitlab.Ptr(contextName),
281281
}
282282

283-
// In case we have access, set the status. Typically, on a Merge Request (MR)
284-
// from a fork in an upstream repository, the token needs to have write access
285-
// to the fork repository in order to create a status. However, the token set on the
286-
// Repository CR usually doesn't have such broad access, preventing from creating
287-
// a status comment on it.
288-
// This would work on a push or an MR from a branch within the same repo.
289-
// Ignoring errors because of the write access issues,
290-
if _, _, err := v.Client().Commits.SetCommitStatus(event.SourceProjectID, event.SHA, opt); err != nil {
291-
v.eventEmitter.EmitMessage(v.repo, zap.ErrorLevel, "FailedToSetCommitStatus",
292-
"cannot set status with the GitLab token because of: "+err.Error())
283+
// setCommitStatus attempts to set the commit status for a given SHA, handling GitLab's permission model.
284+
// First tries the source project (fork), then falls back to the target project (upstream repo).
285+
// This fallback is necessary because:
286+
// - In fork/MR scenarios, the GitLab token may not have write access to the contributor's fork
287+
// - This ensures commit status can be displayed somewhere useful regardless
288+
// of permission differences
289+
// Returns nil if status is set on either project, logs error if both attempts fail.
290+
_, _, err := v.Client().Commits.SetCommitStatus(event.SourceProjectID, event.SHA, opt)
291+
if err == nil {
292+
v.Logger.Debugf("created commit status on source project ID %d", event.SourceProjectID)
293+
return nil
294+
}
295+
if _, _, err2 := v.Client().Commits.SetCommitStatus(event.TargetProjectID, event.SHA, opt); err2 == nil {
296+
v.Logger.Debugf("created commit status on target project ID %d", event.TargetProjectID)
297+
return nil
293298
}
299+
v.Logger.Debugf(
300+
"Failed to create commit status: source project ID %d, target project ID %d. "+
301+
"If you want Gitlab Pipeline Status update, ensure your GitLab token has access "+
302+
"to the source repository. Error: %v",
303+
event.SourceProjectID, event.TargetProjectID, err)
294304

295305
eventType := triggertype.IsPullRequestType(event.EventType)
296306
// When a GitOps command is sent on a pushed commit, it mistakenly treats it as a pull_request

pkg/provider/gitlab/gitlab_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,66 @@ func TestCreateStatus(t *testing.T) {
201201
postStr: "has completed",
202202
},
203203
},
204+
{
205+
name: "commit status success on source project",
206+
wantClient: true,
207+
wantErr: false,
208+
fields: fields{
209+
targetProjectID: 100,
210+
},
211+
args: args{
212+
statusOpts: provider.StatusOpts{
213+
Conclusion: "success",
214+
},
215+
event: &info.Event{
216+
TriggerTarget: "pull_request",
217+
SourceProjectID: 200,
218+
TargetProjectID: 100,
219+
SHA: "abc123",
220+
},
221+
postStr: "has successfully",
222+
},
223+
},
224+
{
225+
name: "commit status falls back to target project",
226+
wantClient: true,
227+
wantErr: false,
228+
fields: fields{
229+
targetProjectID: 100,
230+
},
231+
args: args{
232+
statusOpts: provider.StatusOpts{
233+
Conclusion: "success",
234+
},
235+
event: &info.Event{
236+
TriggerTarget: "pull_request",
237+
SourceProjectID: 404, // Will fail to find this project
238+
TargetProjectID: 100,
239+
SHA: "abc123",
240+
},
241+
postStr: "has successfully",
242+
},
243+
},
244+
{
245+
name: "commit status fails on both projects but continues",
246+
wantClient: true,
247+
wantErr: false,
248+
fields: fields{
249+
targetProjectID: 100,
250+
},
251+
args: args{
252+
statusOpts: provider.StatusOpts{
253+
Conclusion: "success",
254+
},
255+
event: &info.Event{
256+
TriggerTarget: "pull_request",
257+
SourceProjectID: 404, // Will fail
258+
TargetProjectID: 405, // Will fail
259+
SHA: "abc123",
260+
},
261+
postStr: "has successfully",
262+
},
263+
},
204264
}
205265
for _, tt := range tests {
206266
t.Run(tt.name, func(t *testing.T) {
@@ -216,6 +276,7 @@ func TestCreateStatus(t *testing.T) {
216276
v := &Provider{
217277
targetProjectID: tt.fields.targetProjectID,
218278
run: params.New(),
279+
Logger: logger,
219280
pacInfo: &info.PacOpts{
220281
Settings: settings.Settings{
221282
ApplicationName: settings.PACApplicationNameDefaultValue,
@@ -232,6 +293,40 @@ func TestCreateStatus(t *testing.T) {
232293
client, mux, tearDown := thelp.Setup(t)
233294
v.SetGitLabClient(client)
234295
defer tearDown()
296+
297+
// Mock commit status endpoints for both source and target projects
298+
if tt.args.event.SourceProjectID != 0 {
299+
// Mock source project commit status endpoint
300+
sourceStatusPath := fmt.Sprintf("/projects/%d/statuses/%s", tt.args.event.SourceProjectID, tt.args.event.SHA)
301+
mux.HandleFunc(sourceStatusPath, func(rw http.ResponseWriter, _ *http.Request) {
302+
if tt.args.event.SourceProjectID == 404 {
303+
// Simulate failure on source project
304+
rw.WriteHeader(http.StatusNotFound)
305+
fmt.Fprint(rw, `{"message": "404 Project Not Found"}`)
306+
return
307+
}
308+
// Success on source project
309+
rw.WriteHeader(http.StatusCreated)
310+
fmt.Fprint(rw, `{}`)
311+
})
312+
}
313+
314+
if tt.args.event.TargetProjectID != 0 {
315+
// Mock target project commit status endpoint
316+
targetStatusPath := fmt.Sprintf("/projects/%d/statuses/%s", tt.args.event.TargetProjectID, tt.args.event.SHA)
317+
mux.HandleFunc(targetStatusPath, func(rw http.ResponseWriter, _ *http.Request) {
318+
if tt.args.event.TargetProjectID == 404 {
319+
// Simulate failure on target project
320+
rw.WriteHeader(http.StatusNotFound)
321+
fmt.Fprint(rw, `{"message": "404 Project Not Found"}`)
322+
return
323+
}
324+
// Success on target project
325+
rw.WriteHeader(http.StatusCreated)
326+
fmt.Fprint(rw, `{}`)
327+
})
328+
}
329+
235330
thelp.MuxNotePost(t, mux, v.targetProjectID, tt.args.event.PullRequestNumber, tt.args.postStr)
236331
}
237332

@@ -1044,9 +1139,12 @@ func TestGitLabCreateComment(t *testing.T) {
10441139
t.Run(tt.name, func(t *testing.T) {
10451140
fakeclient, mux, teardown := thelp.Setup(t)
10461141
defer teardown()
1142+
observer, _ := zapobserver.New(zap.InfoLevel)
1143+
logger := zap.New(observer).Sugar()
10471144

10481145
if tt.clientNil {
10491146
p := &Provider{
1147+
Logger: logger,
10501148
sourceProjectID: 666,
10511149
}
10521150
err := p.CreateComment(context.Background(), tt.event, tt.commit, tt.updateMarker)

test/gitlab_merge_request_test.go

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/cctx"
2121
tgitlab "github.com/openshift-pipelines/pipelines-as-code/test/pkg/gitlab"
2222
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/payload"
23-
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/repository"
2423
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/scm"
2524
twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait"
2625
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
@@ -29,6 +28,7 @@ import (
2928
"gotest.tools/v3/assert"
3029
corev1 "k8s.io/api/core/v1"
3130
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/util/wait"
3232
"knative.dev/pkg/apis"
3333
)
3434

@@ -500,7 +500,7 @@ func TestGitlabIssueGitopsComment(t *testing.T) {
500500
twait.Succeeded(ctx, t, runcnx, opts, sopt)
501501
}
502502

503-
func TestGitlabDisableCommentsOnMR(t *testing.T) {
503+
func TestGitlabDisableCommentsOnMRNotCreated(t *testing.T) {
504504
targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
505505
ctx := context.Background()
506506
runcnx, opts, glprovider, err := tgitlab.Setup(ctx)
@@ -510,7 +510,7 @@ func TestGitlabDisableCommentsOnMR(t *testing.T) {
510510
projectinfo, resp, err := glprovider.Client().Projects.GetProject(opts.ProjectID, nil)
511511
assert.NilError(t, err)
512512
if resp != nil && resp.StatusCode == http.StatusNotFound {
513-
t.Errorf("Repository %s not found in %s", opts.Organization, opts.Repo)
513+
t.Fatalf("Repository %s not found in %s", opts.Organization, opts.Repo) // Use Fatalf to stop test on critical error
514514
}
515515

516516
settings := v1alpha1.Settings{
@@ -541,9 +541,10 @@ func TestGitlabDisableCommentsOnMR(t *testing.T) {
541541
TargetRefName: targetRefName,
542542
BaseRefName: projectinfo.DefaultBranch,
543543
}
544-
_ = scm.PushFilesToRefGit(t, scmOpts, entries)
544+
// NEW: Capture the commit SHA returned by the push operation.
545+
sha := scm.PushFilesToRefGit(t, scmOpts, entries)
546+
runcnx.Clients.Log.Infof("Commit %s has been created and pushed to branch %s", sha, targetRefName)
545547

546-
runcnx.Clients.Log.Infof("Branch %s has been created and pushed with files", targetRefName)
547548
mrTitle := "TestMergeRequest - " + targetRefName
548549
mrID, err := tgitlab.CreateMR(glprovider.Client(), opts.ProjectID, targetRefName, projectinfo.DefaultBranch, mrTitle)
549550
assert.NilError(t, err)
@@ -555,13 +556,60 @@ func TestGitlabDisableCommentsOnMR(t *testing.T) {
555556
OnEvent: "Merge Request",
556557
TargetNS: targetNS,
557558
NumberofPRMatch: 1,
558-
SHA: "",
559+
SHA: sha, // NEW: Pass the captured SHA to ensure we wait for the correct PipelineRun
559560
}
560561
twait.Succeeded(ctx, t, runcnx, opts, sopt)
561562
prsNew, err := runcnx.Clients.Tekton.TektonV1().PipelineRuns(targetNS).List(ctx, metav1.ListOptions{})
562563
assert.NilError(t, err)
563564
assert.Assert(t, len(prsNew.Items) == 1)
564565

566+
runcnx.Clients.Log.Infof("Checking status of GitLab pipeline for commit: %s", sha)
567+
// Define constants for polling
568+
const pipelineCheckTimeout = 5 * time.Minute
569+
const pipelineCheckInterval = 10 * time.Second
570+
571+
var pipeline *clientGitlab.Pipeline
572+
// Use a polling mechanism to wait for the pipeline to succeed.
573+
err = wait.PollUntilContextTimeout(ctx, pipelineCheckInterval, pipelineCheckTimeout, true, func(_ context.Context) (bool, error) {
574+
// Find the pipeline associated with our specific commit SHA
575+
pipelines, _, listErr := glprovider.Client().Pipelines.ListProjectPipelines(opts.ProjectID, &clientGitlab.ListProjectPipelinesOptions{
576+
SHA: &sha,
577+
})
578+
if listErr != nil {
579+
return false, listErr // Propagate API errors
580+
}
581+
if len(pipelines) == 0 {
582+
runcnx.Clients.Log.Info("Waiting for pipeline to be created...")
583+
return false, nil // Pipeline not found yet, continue polling
584+
}
585+
if len(pipelines) > 1 {
586+
// This is unexpected, fail fast
587+
return false, fmt.Errorf("expected 1 pipeline for SHA %s, but found %d", sha, len(pipelines))
588+
}
589+
590+
// Get the latest status of our specific pipeline
591+
p, _, getErr := glprovider.Client().Pipelines.GetPipeline(opts.ProjectID, pipelines[0].ID)
592+
if getErr != nil {
593+
return false, getErr
594+
}
595+
596+
runcnx.Clients.Log.Infof("Current pipeline status: %s", p.Status)
597+
switch p.Status {
598+
case "success":
599+
pipeline = p
600+
return true, nil // Success! Stop polling.
601+
case "failed", "canceled", "skipped":
602+
// The pipeline has finished but not successfully.
603+
return false, fmt.Errorf("pipeline finished with non-success status: %s", p.Status)
604+
default:
605+
// The pipeline is still running or pending, continue polling.
606+
return false, nil
607+
}
608+
})
609+
assert.NilError(t, err, "failed while waiting for GitLab pipeline to succeed")
610+
assert.Equal(t, "success", pipeline.Status, "The final pipeline status was not 'success'")
611+
runcnx.Clients.Log.Infof("✅ GitLab pipeline ID %d has succeeded!", pipeline.ID)
612+
565613
// No comments will be added related to pipelineruns info
566614
notes, _, err := glprovider.Client().Notes.ListMergeRequestNotes(opts.ProjectID, mrID, nil)
567615

@@ -574,46 +622,8 @@ func TestGitlabDisableCommentsOnMR(t *testing.T) {
574622
}
575623
}
576624
// Since Gitlab comment strategy is disabled,
577-
// no comments will be posted related to pipelineruns
625+
// no comments will be posted related to PipelineRuns
578626
assert.Equal(t, 0, successCommentsPost)
579-
580-
// Update the repo setting to nil, and comment /test to restart the pipelinerun
581-
// and now comments will be added on the MR
582-
waitOpts := twait.Opts{
583-
RepoName: targetNS,
584-
Namespace: targetNS,
585-
TargetSHA: "",
586-
}
587-
588-
runcnx.Clients.Log.Info("Updating Gitlab Comment Strategy to nil...")
589-
err = repository.UpdateRepo(ctx, waitOpts.Namespace, waitOpts.RepoName, runcnx.Clients)
590-
assert.NilError(t, err)
591-
592-
runcnx.Clients.Log.Infof("Sending /test comment on MergeRequest %s/-/merge_requests/%d", projectinfo.WebURL, mrID)
593-
_, _, err = glprovider.Client().Notes.CreateMergeRequestNote(opts.ProjectID, mrID, &clientGitlab.CreateMergeRequestNoteOptions{
594-
Body: clientGitlab.Ptr("/test"),
595-
})
596-
assert.NilError(t, err)
597-
598-
sopt = twait.SuccessOpt{
599-
Title: commitTitle,
600-
OnEvent: opscomments.TestAllCommentEventType.String(),
601-
TargetNS: targetNS,
602-
NumberofPRMatch: 2, // this is the max we get in repos status
603-
SHA: "",
604-
}
605-
runcnx.Clients.Log.Info("Checking that PAC has posted successful comments for all PR that has been tested")
606-
twait.Succeeded(ctx, t, runcnx, opts, sopt)
607-
608-
notes, _, err = glprovider.Client().Notes.ListMergeRequestNotes(opts.ProjectID, mrID, nil)
609-
assert.NilError(t, err)
610-
successCommentsPost = 0
611-
for _, n := range notes {
612-
if commentRegexp.MatchString(n.Body) {
613-
successCommentsPost++
614-
}
615-
}
616-
assert.Equal(t, 2, successCommentsPost)
617627
}
618628

619629
func TestGitlabMergeRequestOnUpdateAtAndLabelChange(t *testing.T) {

test/pkg/scm/scm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func PushFilesToRefGit(t *testing.T, opts *Opts, entries map[string]string) stri
130130
assert.NilError(t, err)
131131

132132
gitPushPullRetry(t, opts, path)
133-
return sha
133+
return strings.TrimSpace(sha)
134134
}
135135

136136
func ChangeFilesRefGit(t *testing.T, opts *Opts, fileChanges []FileChange) {

0 commit comments

Comments
 (0)