Monorepo for Tangled tangled.org

appview/notify: add Push event and webhook notifier #1067

merged opened by anirudh.fi targeting master from icy/qlyxxp

Add Push method to Notifier interface for git push events. Implement WebhookNotifier that sends webhook payloads with HMAC-SHA256 signatures for authentication. Supports push events with delivery tracking and retry logic (3 attempts with exponential backoff).

Signed-off-by: Anirudh Oppiliappan anirudh@tangled.org

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3menyebs7qn22
+34 -39
Interdiff #1 #2
appview/notify/db/db.go

This file has not been changed.

appview/notify/logging_notifier.go

This file has not been changed.

appview/notify/merged_notifier.go

This file has not been changed.

appview/notify/notifier.go

This file has not been changed.

+34 -39
appview/notify/webhook_notifier.go
··· 11 "io" 12 "log/slog" 13 "net/http" 14 - "slices" 15 "time" 16 17 "github.com/avast/retry-go/v4" ··· 49 // check if any webhooks are subscribed to push events 50 var pushWebhooks []models.Webhook 51 for _, webhook := range webhooks { 52 - if slices.Contains(webhook.Events, "push") { 53 pushWebhooks = append(pushWebhooks, webhook) 54 } 55 } ··· 66 67 // Send webhooks 68 for _, webhook := range pushWebhooks { 69 - go w.sendWebhook(ctx, webhook, "push", payload) 70 } 71 } 72 73 - // buildPushPayload creates a the webhook payload 74 - func (w *WebhookNotifier) buildPushPayload(repo *models.Repo, ref, oldSha, newSha, committerDid string) (map[string]interface{}, error) { 75 owner := repo.Did 76 77 pusher := committerDid ··· 79 pusher = owner 80 } 81 82 - // build repository object 83 - repository := map[string]any{ 84 - "name": repo.Name, 85 - "full_name": fmt.Sprintf("%s/%s", repo.Did, repo.Name), 86 - "description": repo.Description, 87 - "fork": repo.Source != "", 88 - "html_url": fmt.Sprintf("https://%s/%s/%s", repo.Knot, repo.Did, repo.Name), 89 - "clone_url": fmt.Sprintf("https://%s/%s/%s", repo.Knot, repo.Did, repo.Name), 90 - "ssh_url": fmt.Sprintf("ssh://git@%s/%s/%s", repo.Knot, repo.Did, repo.Name), 91 - "created_at": repo.Created.Format(time.RFC3339), 92 - "updated_at": repo.Created.Format(time.RFC3339), 93 } 94 95 - // add optional fields if available 96 if repo.Website != "" { 97 - repository["website"] = repo.Website 98 } 99 if repo.RepoStats != nil { 100 - repository["stars_count"] = repo.RepoStats.StarCount 101 - repository["open_issues_count"] = repo.RepoStats.IssueCount.Open 102 - } 103 - 104 - ownerObj := map[string]any{ 105 - "did": owner, 106 } 107 108 - repository["owner"] = ownerObj 109 - 110 - pusherObj := map[string]interface{}{ 111 - "did": pusher, 112 - } 113 - 114 - // final payload 115 - payload := map[string]interface{}{ 116 - "ref": ref, 117 - "before": oldSha, 118 - "after": newSha, 119 - "repository": repository, 120 - "pusher": pusherObj, 121 } 122 123 return payload, nil 124 } 125 126 // sendWebhook sends the webhook http request 127 - func (w *WebhookNotifier) sendWebhook(ctx context.Context, webhook models.Webhook, event string, payload map[string]interface{}) { 128 deliveryId := uuid.New().String() 129 130 payloadBytes, err := json.Marshal(payload) ··· 139 return 140 } 141 142 - shortSha := payload["after"].(string)[:7] 143 144 req.Header.Set("Content-Type", "application/json") 145 req.Header.Set("User-Agent", "Tangled-Hook/"+shortSha) 146 req.Header.Set("X-Tangled-Event", event) 147 req.Header.Set("X-Tangled-Hook-ID", fmt.Sprintf("%d", webhook.Id)) 148 req.Header.Set("X-Tangled-Delivery", deliveryId) 149 150 if webhook.Secret != "" { 151 signature := w.computeSignature(payloadBytes, webhook.Secret)
··· 11 "io" 12 "log/slog" 13 "net/http" 14 "time" 15 16 "github.com/avast/retry-go/v4" ··· 48 // check if any webhooks are subscribed to push events 49 var pushWebhooks []models.Webhook 50 for _, webhook := range webhooks { 51 + if webhook.HasEvent(models.WebhookEventPush) { 52 pushWebhooks = append(pushWebhooks, webhook) 53 } 54 } ··· 65 66 // Send webhooks 67 for _, webhook := range pushWebhooks { 68 + go w.sendWebhook(ctx, webhook, string(models.WebhookEventPush), payload) 69 } 70 } 71 72 + // buildPushPayload creates the webhook payload 73 + func (w *WebhookNotifier) buildPushPayload(repo *models.Repo, ref, oldSha, newSha, committerDid string) (*models.WebhookPayload, error) { 74 owner := repo.Did 75 76 pusher := committerDid ··· 78 pusher = owner 79 } 80 81 + // Build repository object 82 + repository := models.WebhookRepository{ 83 + Name: repo.Name, 84 + FullName: fmt.Sprintf("%s/%s", repo.Did, repo.Name), 85 + Description: repo.Description, 86 + Fork: repo.Source != "", 87 + HtmlUrl: fmt.Sprintf("https://%s/%s/%s", repo.Knot, repo.Did, repo.Name), 88 + CloneUrl: fmt.Sprintf("https://%s/%s/%s", repo.Knot, repo.Did, repo.Name), 89 + SshUrl: fmt.Sprintf("ssh://git@%s/%s/%s", repo.Knot, repo.Did, repo.Name), 90 + CreatedAt: repo.Created.Format(time.RFC3339), 91 + UpdatedAt: repo.Created.Format(time.RFC3339), 92 + Owner: models.WebhookUser{ 93 + Did: owner, 94 + }, 95 } 96 97 + // Add optional fields 98 if repo.Website != "" { 99 + repository.Website = repo.Website 100 } 101 if repo.RepoStats != nil { 102 + repository.StarsCount = repo.RepoStats.StarCount 103 + repository.OpenIssues = repo.RepoStats.IssueCount.Open 104 } 105 106 + // Build payload 107 + payload := &models.WebhookPayload{ 108 + Ref: ref, 109 + Before: oldSha, 110 + After: newSha, 111 + Repository: repository, 112 + Pusher: models.WebhookUser{ 113 + Did: pusher, 114 + }, 115 } 116 117 return payload, nil 118 } 119 120 // sendWebhook sends the webhook http request 121 + func (w *WebhookNotifier) sendWebhook(ctx context.Context, webhook models.Webhook, event string, payload *models.WebhookPayload) { 122 deliveryId := uuid.New().String() 123 124 payloadBytes, err := json.Marshal(payload) ··· 133 return 134 } 135 136 + shortSha := payload.After[:7] 137 138 req.Header.Set("Content-Type", "application/json") 139 req.Header.Set("User-Agent", "Tangled-Hook/"+shortSha) 140 req.Header.Set("X-Tangled-Event", event) 141 req.Header.Set("X-Tangled-Hook-ID", fmt.Sprintf("%d", webhook.Id)) 142 req.Header.Set("X-Tangled-Delivery", deliveryId) 143 + req.Header.Set("X-Tangled-Repo", payload.Repository.FullName) 144 145 if webhook.Secret != "" { 146 signature := w.computeSignature(payloadBytes, webhook.Secret)

History

6 rounds 3 comments
sign up or login to add to the discussion
1 commit
expand
appview/notify: add Push event and webhook notifier
3/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
appview/notify: add Push event and webhook notifier
3/3 success
expand
expand 0 comments
1 commit
expand
appview/notify: add Push event and webhook notifier
3/3 success
expand
expand 0 comments
1 commit
expand
appview/notify: add Push event and webhook notifier
3/3 success
expand
expand 0 comments
1 commit
expand
appview/notify: add Push event and webhook notifier
3/3 success
expand
expand 3 comments
  • would be nice to have a strongly typed payload object here
  • the value for user-agent seems a bit strange, its Tangled-Hook/<short-sha>, would it be better to identify the repo here?

changeset lgtm otherwise!

GitHub does the same for their hook version. I suppose we could have a separate header with the repository info?

makes sense, let's do the same then

1 commit
expand
appview/notify: add Push event and webhook notifier
3/3 success
expand
expand 0 comments