Skip to content

Commit 239f25f

Browse files
chmouelzakisk
authored andcommitted
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 <[email protected]>
1 parent 542f1a0 commit 239f25f

File tree

6 files changed

+211
-73
lines changed

6 files changed

+211
-73
lines changed

docs/content/docs/guide/repositorycrd.md

Lines changed: 24 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:
@@ -123,7 +124,29 @@ access to the infrastructure.
123124

124125
## Disabling all comments for PipelineRuns on GitLab MR
125126

126-
`comment_strategy` allows you to disable the comments on GitLab MR for a Repository
127+
By default, Pipelines-as-Code attempts to update the commit status through the
128+
GitLab API. It first tries the source project (fork), then falls back to the
129+
target project (upstream repository). The source project update succeeds when
130+
the configured token has access to the source repository and GitLab creates
131+
pipeline entries for external CI systems like Pipelines-as-Code. The target
132+
project fallback may fail if there's no CI pipeline running for that commit
133+
in the target repository, since GitLab only creates pipeline entries for commits
134+
that actually trigger CI in that specific project. If either status update
135+
succeeds, no comment is posted on the Merge Request.
136+
137+
When a status update succeeds, you can see the status in the GitLab UI in the `Pipelines` tab, as
138+
shown in the following example:
139+
140+
![Gitlab Pipelines from Pipelines-as-Code](/images/gitlab-pipelines-tab.png)
141+
142+
Comments are only posted when:
143+
144+
- Both commit status updates fail (typically due to insufficient token permissions)
145+
- The event type and repository settings allow commenting
146+
- The `comment_strategy` is not set to `disable_all`
147+
148+
To explicitly disable all comments on GitLab Merge Requests for a Repository,
149+
use the `comment_strategy` setting:
127150

128151
```yaml
129152
spec:
@@ -132,8 +155,6 @@ spec:
132155
comment_strategy: "disable_all"
133156
```
134157

135-
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
136-
137158
## Disabling all comments for PipelineRuns in GitHub Pull Requests on GitHub Webhook Setup
138159

139160
`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)

0 commit comments

Comments
 (0)