Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions pkg/provider/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gitlab
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -318,12 +319,26 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusOpts
v.Logger.Debugf("created commit status on source project ID %d", event.TargetProjectID)
return nil
}
if _, _, err2 := v.Client().Commits.SetCommitStatus(event.TargetProjectID, event.SHA, opt); err2 == nil {
_, _, err2 := v.Client().Commits.SetCommitStatus(event.TargetProjectID, event.SHA, opt)
if err2 == nil {
v.Logger.Debugf("created commit status on target project ID %d", event.TargetProjectID)
// we managed to set the status on the target repo, all good we are done
return nil
}
v.Logger.Debugf("cannot set status with the GitLab token on the target project: %v", err)
v.Logger.Debugf("cannot set status with the GitLab token on the target project: %v", err2)

// Only fall back to creating MR comments if either error is a permission issue (401/403).
// For other errors (e.g., state transition errors like "Cannot transition status via :run from :running"),
// the status might already be set, so we should not create a comment.
isPermErr := func(e error) bool {
var errResp *gitlab.ErrorResponse
return errors.As(e, &errResp) && (errResp.Response.StatusCode == http.StatusUnauthorized || errResp.Response.StatusCode == http.StatusForbidden)
}
if !isPermErr(err) && !isPermErr(err2) {
v.Logger.Debugf("skipping MR comment as error is not a permission issue")
return nil
}

// we only show the first error as it's likely something the user has more control to fix
// the second err is cryptic as it needs a dummy gitlab pipeline to start
// with and will only give more confusion in the event namespace
Expand Down
86 changes: 71 additions & 15 deletions pkg/provider/gitlab/gitlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func TestCreateStatus(t *testing.T) {
},
},
{
name: "commit status fails on both projects but continues",
name: "commit status fails on both projects with non-permission error skips MR comment",
wantClient: true,
wantErr: false,
fields: fields{
Expand All @@ -270,10 +270,50 @@ func TestCreateStatus(t *testing.T) {
},
event: &info.Event{
TriggerTarget: "pull_request",
SourceProjectID: 404, // Will fail
TargetProjectID: 405, // Will fail
SourceProjectID: 400, // Will fail with 400 (not permission error)
TargetProjectID: 405, // Will fail with 400 (not permission error)
SHA: "abc123",
},
postStr: "", // No MR comment expected for non-permission errors
},
},
{
name: "permission error 403 on source creates MR comment",
wantClient: true,
wantErr: false,
fields: fields{
targetProjectID: 100,
},
args: args{
statusOpts: provider.StatusOpts{
Conclusion: "success",
},
event: &info.Event{
TriggerTarget: "pull_request",
SourceProjectID: 403, // Will fail with 403 Forbidden
TargetProjectID: 400, // Will fail with 400 (not permission error)
SHA: "abc123def",
},
postStr: "has successfully",
},
},
{
name: "permission error 401 on source creates MR comment",
wantClient: true,
wantErr: false,
fields: fields{
targetProjectID: 100,
},
args: args{
statusOpts: provider.StatusOpts{
Conclusion: "success",
},
event: &info.Event{
TriggerTarget: "pull_request",
SourceProjectID: 401, // Will fail with 401 Unauthorized
TargetProjectID: 400, // Will fail with 400 (not permission error)
SHA: "abc123ghi",
},
postStr: "has successfully",
},
},
Expand Down Expand Up @@ -315,31 +355,47 @@ func TestCreateStatus(t *testing.T) {
// Mock source project commit status endpoint
sourceStatusPath := fmt.Sprintf("/projects/%d/statuses/%s", tt.args.event.SourceProjectID, tt.args.event.SHA)
mux.HandleFunc(sourceStatusPath, func(rw http.ResponseWriter, _ *http.Request) {
if tt.args.event.SourceProjectID == 404 {
// Simulate failure on source project
switch tt.args.event.SourceProjectID {
case 400:
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprint(rw, `{"message": "400 Bad Request"}`)
case 401:
rw.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(rw, `{"message": "401 Unauthorized"}`)
case 403:
rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, `{"message": "403 Forbidden"}`)
case 404:
rw.WriteHeader(http.StatusNotFound)
fmt.Fprint(rw, `{"message": "404 Project Not Found"}`)
return
default:
rw.WriteHeader(http.StatusCreated)
fmt.Fprint(rw, `{}`)
}
// Success on source project
rw.WriteHeader(http.StatusCreated)
fmt.Fprint(rw, `{}`)
})
}

if tt.args.event.TargetProjectID != 0 {
// Mock target project commit status endpoint
targetStatusPath := fmt.Sprintf("/projects/%d/statuses/%s", tt.args.event.TargetProjectID, tt.args.event.SHA)
mux.HandleFunc(targetStatusPath, func(rw http.ResponseWriter, _ *http.Request) {
if tt.args.event.TargetProjectID == 404 {
// Simulate failure on target project
switch tt.args.event.TargetProjectID {
case 400, 405:
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprint(rw, `{"message": "400 Bad Request"}`)
case 401:
rw.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(rw, `{"message": "401 Unauthorized"}`)
case 403:
rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, `{"message": "403 Forbidden"}`)
case 404:
rw.WriteHeader(http.StatusNotFound)
fmt.Fprint(rw, `{"message": "404 Project Not Found"}`)
return
default:
rw.WriteHeader(http.StatusCreated)
fmt.Fprint(rw, `{}`)
}
// Success on target project
rw.WriteHeader(http.StatusCreated)
fmt.Fprint(rw, `{}`)
})
}

Expand Down