Skip to content

Commit 8c5380b

Browse files
committed
fix: Ignore 422 response while creating commit statuses
Signed-off-by: Ragnar Paide <[email protected]>
1 parent e3fe626 commit 8c5380b

File tree

5 files changed

+112
-4
lines changed

5 files changed

+112
-4
lines changed

docs/services/github.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,6 @@ template.app-deployed: |
111111
- The `github.pullRequestComment.commentTag` parameter is used to identify the comment. If a comment with the specified tag is found, it will be updated (upserted). If no comment with the tag is found, a new comment will be created.
112112
- Reference is optional. When set, it will be used as the ref to deploy. If not set, the revision will be used as the ref to deploy.
113113

114+
## Commit Statuses
115+
116+
The [method for generating commit statuses](https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status) allows a maximum of 1000 attempts using the same commit SHA and context. Once this limit is reached, the API returns validation errors (HTTP 422). The notification engine ignores these errors and marks the notification attempts as completed.

pkg/controller/controller.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,15 @@ func (c *notificationController) processResourceWithAPI(api api.API, resource v1
232232
if err := api.Send(un.Object, cr.Templates, to); err != nil {
233233
logEntry.Errorf("Failed to notify recipient %s defined in resource %s/%s: %v using the configuration in namespace %s",
234234
to, resource.GetNamespace(), resource.GetName(), err, apiNamespace)
235-
notificationsState.SetAlreadyNotified(c.isSelfServiceConfigureApi(api), apiNamespace, trigger, cr, to, false)
236-
c.metricsRegistry.IncDeliveriesCounter(trigger, to.Service, false)
237-
eventSequence.addError(fmt.Errorf("failed to deliver notification %s to %s: %v using the configuration in namespace %s", trigger, to, err, apiNamespace))
235+
236+
retry := services.HandleSendError(err, logEntry)
237+
238+
if retry {
239+
notificationsState.SetAlreadyNotified(c.isSelfServiceConfigureApi(api), apiNamespace, trigger, cr, to, false)
240+
c.metricsRegistry.IncDeliveriesCounter(trigger, to.Service, false)
241+
eventSequence.addError(fmt.Errorf("failed to deliver notification %s to %s: %v using the configuration in namespace %s", trigger, to, err, apiNamespace))
242+
}
243+
238244
} else {
239245
logEntry.Debugf("Notification %s was sent using the configuration in namespace %s", to.Recipient, apiNamespace)
240246
c.metricsRegistry.IncDeliveriesCounter(trigger, to.Service, true)

pkg/controller/controller_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,79 @@ func TestDoesNotSendNotificationIfAnnotationPresent(t *testing.T) {
188188
assert.NoError(t, err)
189189
}
190190

191+
func TestDoesNotSendNotificationIfTooManyCommitStatusesReceived(t *testing.T) {
192+
ctx, cancel := context.WithCancel(context.TODO())
193+
defer cancel()
194+
195+
setAnnotations := func(notifiedAnnoationKeyValue string) func(obj *unstructured.Unstructured) {
196+
return withAnnotations(map[string]string{
197+
subscriptions.SubscribeAnnotationKey("my-trigger", "mock"): "recipient",
198+
notifiedAnnotationKey: notifiedAnnoationKeyValue,
199+
})
200+
}
201+
202+
state := NotificationsState{}
203+
_ = state.SetAlreadyNotified(false, "", "my-trigger", triggers.ConditionResult{}, services.Destination{Service: "mock", Recipient: "recipient"}, false)
204+
app := newResource("test", setAnnotations(mustToJson(state)))
205+
ctrl, api, err := newController(t, ctx, newFakeClient(app))
206+
assert.NoError(t, err)
207+
208+
api.EXPECT().GetConfig().Return(notificationApi.Config{}).AnyTimes()
209+
api.EXPECT().RunTrigger("my-trigger", gomock.Any()).Return([]triggers.ConditionResult{{Triggered: true, Templates: []string{"test"}}}, nil).Times(2)
210+
api.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).Return(&services.TooManyGitHubCommitStatusesError{Sha: "sha", Context: "context"}).Times(1)
211+
212+
// First attempt should hit the TooManyCommitStatusesError.
213+
// Returned annotations1 should contain the information about processed notification
214+
// as a result of hitting the ToomanyCommitStatusesError error.
215+
annotations1, err := ctrl.processResourceWithAPI(api, app, logEntry, &NotificationEventSequence{})
216+
217+
assert.NoError(t, err)
218+
219+
// Persist the notification state in the annotations.
220+
setAnnotations(annotations1[notifiedAnnotationKey])(app)
221+
222+
// The second attempt should see that the notification has already been processed
223+
// and the value of the notification annotation should not change. In the second attempt api.Send is not called.
224+
annotations2, err := ctrl.processResourceWithAPI(api, app, logEntry, &NotificationEventSequence{})
225+
226+
assert.NoError(t, err)
227+
assert.Equal(t, annotations1[notifiedAnnotationKey], annotations2[notifiedAnnotationKey])
228+
}
229+
230+
func TestRetriesNotificationIfSendThrows(t *testing.T) {
231+
ctx, cancel := context.WithCancel(context.TODO())
232+
defer cancel()
233+
234+
setAnnotations := func(notifiedAnnoationKeyValue string) func(obj *unstructured.Unstructured) {
235+
return withAnnotations(map[string]string{
236+
subscriptions.SubscribeAnnotationKey("my-trigger", "mock"): "recipient",
237+
notifiedAnnotationKey: notifiedAnnoationKeyValue,
238+
})
239+
}
240+
241+
state := NotificationsState{}
242+
_ = state.SetAlreadyNotified(false, "", "my-trigger", triggers.ConditionResult{}, services.Destination{Service: "mock", Recipient: "recipient"}, false)
243+
app := newResource("test", setAnnotations(mustToJson(state)))
244+
ctrl, api, err := newController(t, ctx, newFakeClient(app))
245+
assert.NoError(t, err)
246+
247+
api.EXPECT().GetConfig().Return(notificationApi.Config{}).AnyTimes()
248+
api.EXPECT().RunTrigger("my-trigger", gomock.Any()).Return([]triggers.ConditionResult{{Triggered: true, Templates: []string{"test"}}}, nil).Times(2)
249+
api.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("boom")).Times(2)
250+
251+
// First attempt. The returned annotations should not contain the notification state due to the error.
252+
annotations, err := ctrl.processResourceWithAPI(api, app, logEntry, &NotificationEventSequence{})
253+
254+
assert.NoError(t, err)
255+
assert.Empty(t, annotations[notifiedAnnotationKey])
256+
257+
// Second attempt. The returned annotations should not contain the notification state due to the error.
258+
annotations, err = ctrl.processResourceWithAPI(api, app, logEntry, &NotificationEventSequence{})
259+
260+
assert.NoError(t, err)
261+
assert.Empty(t, annotations[notifiedAnnotationKey])
262+
}
263+
191264
func TestRemovesAnnotationIfNoTrigger(t *testing.T) {
192265
ctx, cancel := context.WithCancel(context.TODO())
193266
defer cancel()

pkg/services/github.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package services
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"net/http"
89
"regexp"
@@ -82,6 +83,15 @@ type GitHubPullRequestComment struct {
8283
CommentTag string `json:"commentTag,omitempty"`
8384
}
8485

86+
type TooManyGitHubCommitStatusesError struct {
87+
Sha string
88+
Context string
89+
}
90+
91+
func (e *TooManyGitHubCommitStatusesError) Error() string {
92+
return fmt.Sprintf("too many commit statuses for sha %s and context %s", e.Sha, e.Context)
93+
}
94+
8595
const (
8696
repoURLtemplate = "{{.app.spec.source.repoURL}}"
8797
revisionTemplate = "{{.app.status.operationState.syncResult.revision}}"
@@ -519,7 +529,11 @@ func (g gitHubService) Send(notification Notification, _ Destination) error {
519529
TargetURL: &notification.GitHub.Status.TargetURL,
520530
},
521531
)
522-
if err != nil {
532+
533+
var ghErr *github.ErrorResponse
534+
if ok := errors.As(err, &ghErr); ok && ghErr.Response.StatusCode == 422 {
535+
return &TooManyGitHubCommitStatusesError{Sha: notification.GitHub.revision, Context: notification.GitHub.Status.Label}
536+
} else if err != nil {
523537
return err
524538
}
525539
}

pkg/services/services.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
texttemplate "text/template"
99
_ "time/tzdata"
1010

11+
"github.com/sirupsen/logrus"
1112
"sigs.k8s.io/yaml"
1213
)
1314

@@ -113,6 +114,17 @@ type NotificationService interface {
113114
Send(notification Notification, dest Destination) error
114115
}
115116

117+
// HandleSendError inspects the error and signals the caller if the Send operation should be retried.
118+
func HandleSendError(err error, logEntry *logrus.Entry) (retry bool) {
119+
switch e := err.(type) {
120+
case *TooManyGitHubCommitStatusesError:
121+
logEntry.Warn(e)
122+
return false
123+
default:
124+
return true
125+
}
126+
}
127+
116128
func NewService(serviceType string, optsData []byte) (NotificationService, error) {
117129
switch serviceType {
118130
case "awssqs":

0 commit comments

Comments
 (0)