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 11 "io" 12 12 "log/slog" 13 13 "net/http" 14 - "slices" 15 14 "time" 16 15 17 16 "github.com/avast/retry-go/v4" ··· 49 48 // check if any webhooks are subscribed to push events 50 49 var pushWebhooks []models.Webhook 51 50 for _, webhook := range webhooks { 52 - if slices.Contains(webhook.Events, "push") { 51 + if webhook.HasEvent(models.WebhookEventPush) { 53 52 pushWebhooks = append(pushWebhooks, webhook) 54 53 } 55 54 } ··· 66 65 67 66 // Send webhooks 68 67 for _, webhook := range pushWebhooks { 69 - go w.sendWebhook(ctx, webhook, "push", payload) 68 + go w.sendWebhook(ctx, webhook, string(models.WebhookEventPush), payload) 70 69 } 71 70 } 72 71 73 - // buildPushPayload creates a the webhook payload 74 - func (w *WebhookNotifier) buildPushPayload(repo *models.Repo, ref, oldSha, newSha, committerDid string) (map[string]interface{}, error) { 72 + // buildPushPayload creates the webhook payload 73 + func (w *WebhookNotifier) buildPushPayload(repo *models.Repo, ref, oldSha, newSha, committerDid string) (*models.WebhookPayload, error) { 75 74 owner := repo.Did 76 75 77 76 pusher := committerDid ··· 79 78 pusher = owner 80 79 } 81 80 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), 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 + }, 93 95 } 94 96 95 - // add optional fields if available 97 + // Add optional fields 96 98 if repo.Website != "" { 97 - repository["website"] = repo.Website 99 + repository.Website = repo.Website 98 100 } 99 101 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, 102 + repository.StarsCount = repo.RepoStats.StarCount 103 + repository.OpenIssues = repo.RepoStats.IssueCount.Open 106 104 } 107 105 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, 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 + }, 121 115 } 122 116 123 117 return payload, nil 124 118 } 125 119 126 120 // sendWebhook sends the webhook http request 127 - func (w *WebhookNotifier) sendWebhook(ctx context.Context, webhook models.Webhook, event string, payload map[string]interface{}) { 121 + func (w *WebhookNotifier) sendWebhook(ctx context.Context, webhook models.Webhook, event string, payload *models.WebhookPayload) { 128 122 deliveryId := uuid.New().String() 129 123 130 124 payloadBytes, err := json.Marshal(payload) ··· 139 133 return 140 134 } 141 135 142 - shortSha := payload["after"].(string)[:7] 136 + shortSha := payload.After[:7] 143 137 144 138 req.Header.Set("Content-Type", "application/json") 145 139 req.Header.Set("User-Agent", "Tangled-Hook/"+shortSha) 146 140 req.Header.Set("X-Tangled-Event", event) 147 141 req.Header.Set("X-Tangled-Hook-ID", fmt.Sprintf("%d", webhook.Id)) 148 142 req.Header.Set("X-Tangled-Delivery", deliveryId) 143 + req.Header.Set("X-Tangled-Repo", payload.Repository.FullName) 149 144 150 145 if webhook.Secret != "" { 151 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