From bc6c776f3e37773cb2ec496de9b66873f1119c68 Mon Sep 17 00:00:00 2001 From: Shivansh Rai Date: Wed, 2 Jul 2025 18:55:11 +0530 Subject: [PATCH 1/2] Add DevRev webhooks demo --- webhooks/README.md | 340 +++++++++++++++++++++++++++++++++++++++++++++ webhooks/go.mod | 5 + webhooks/go.sum | 2 + webhooks/main.go | 85 ++++++++++++ 4 files changed, 432 insertions(+) create mode 100644 webhooks/README.md create mode 100644 webhooks/go.mod create mode 100644 webhooks/go.sum create mode 100644 webhooks/main.go diff --git a/webhooks/README.md b/webhooks/README.md new file mode 100644 index 0000000..cc22c39 --- /dev/null +++ b/webhooks/README.md @@ -0,0 +1,340 @@ +# DevRev Webhooks Test Server + +This is a simple server to test DevRev webhooks. It handles webhook verification +and processes webhook events for work created, updated, and deleted. +Detailed documentation on webhooks can be found [here](https://developer.devrev.ai/public/guides/webhooks). + +## Setup + +1. Start ngrok: +```bash +ngrok http 3000 +``` + +2. Register your webhook with DevRev using the ngrok URL: +```bash +curl --request POST 'https://api.devrev.ai/webhooks.create' \ + --header "Authorization: Bearer $DEVREV_TOKEN" \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "event_types": [ + "work_created", + "work_updated", + "work_deleted" + ], + "url": "your-ngrok-url" + }' +``` + +3. Start the webhook server: + +The `WEBHOOK_SECRET` environment variable is required. It will be available as +`secret` in the response of the above `webhooks.create` API call. + +```bash +WEBHOOK_SECRET=your_webhook_secret go run main.go +``` + +## Example webhook payload + +The following is an example of a `work_updated` webhook payload. Note that +`work_updated` contains `old_work` and `work` fields. + +``` +{ + "id": "don:integration:dvrv-us-1:devo/11FVC3ScK:webhook/1jGzMh1N:webhook_event/U7gbBDxjiWM", + "timestamp": "2025-07-02T13:07:40.937074Z", + "webhook_id": "don:integration:dvrv-us-1:devo/11FVC3ScK:webhook/1jGzMh1N", + "work_updated": { + "old_work": { + "type": "issue", + "applies_to_part": { + "type": "product", + "display_id": "PROD-2", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:product/2", + "id_v1": "don:DEV-11FVC3ScK:product:2", + "name": "prod-foo", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "shivansh-rai", + "display_id": "DEVU-1", + "display_name": "shivansh-rai", + "email": "shivansh.rai@devrev.ai", + "full_name": "Shivansh Rai", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/1", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-1", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Shivansh%20Rai.png" + } + ], + "stage": { + "name": "" + } + }, + "body": "\u003cdon:core:dvrv-us-1:devo/11FVC3ScK:issue/98\u003e", + "created_by": { + "type": "dev_user", + "display_handle": "shivansh-rai", + "display_id": "DEVU-1", + "display_name": "shivansh-rai", + "email": "shivansh.rai@devrev.ai", + "full_name": "Shivansh Rai", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/1", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-1", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Shivansh%20Rai.png" + }, + "created_date": "2025-06-14T13:20:46.963Z", + "custom_fields": { + "ctype__color": "Blue", + "tnt__a_rich_text_field": "A new issue \u003cdon:core:dvrv-us-1:devo/11FVC3ScK:issue/91\u003e", + "tnt__a_text_field": "" + }, + "custom_schema_fragments": [ + "don:core:dvrv-us-1:devo/11FVC3ScK:tenant_fragment/503", + "don:core:dvrv-us-1:devo/11FVC3ScK:custom_type_fragment/480" + ], + "display_id": "ISS-98", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:issue/98", + "id_v1": "don:DEV-11FVC3ScK:issue:98", + "modified_by": { + "type": "dev_user", + "display_handle": "shivansh-rai", + "display_id": "DEVU-1", + "display_name": "shivansh-rai", + "email": "shivansh.rai@devrev.ai", + "full_name": "Shivansh Rai", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/1", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-1", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Shivansh%20Rai.png" + }, + "modified_date": "2025-07-02T13:04:12.1Z", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "gowtham-tg", + "display_id": "DEVU-10", + "display_name": "gowtham-tg", + "email": "gowtham.tg@devrev.ai", + "full_name": "Gowtham Gopinath", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/10", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-10", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Gowtham%20Gopinath.png" + } + ], + "priority": "p1", + "priority_v2": { + "id": 2, + "label": "P1", + "ordinal": 2 + }, + "references": [ + { + "type": "issue", + "display_id": "ISS-98", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:issue/98", + "id_v1": "don:DEV-11FVC3ScK:issue:98", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "gowtham-tg", + "display_id": "DEVU-10", + "display_name": "gowtham-tg", + "email": "gowtham.tg@devrev.ai", + "full_name": "Gowtham Gopinath", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/10", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-10", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Gowtham%20Gopinath.png" + } + ], + "priority": "p2", + "priority_v2": { + "id": 3, + "label": "P2", + "ordinal": 3 + }, + "stage": { + "name": "prioritized", + "stage": { + "display_name": "Prioritized", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:custom_stage/27", + "name": "prioritized" + } + }, + "title": "issue with a subtype" + } + ], + "stage": { + "display_name": "Prioritized", + "name": "prioritized", + "notes": "", + "ordinal": 3600, + "stage": { + "display_name": "Prioritized", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:custom_stage/27", + "name": "prioritized" + }, + "state": { + "display_name": "Open", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:custom_state/1", + "is_final": false, + "name": "open" + } + }, + "state": "open", + "stock_schema_fragment": "don:core:dvrv-us-1:stock_sf/5774789", + "subtype": "test", + "title": "issue with a subtype" + }, + "work": { + "type": "issue", + "applies_to_part": { + "type": "product", + "display_id": "PROD-2", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:product/2", + "id_v1": "don:DEV-11FVC3ScK:product:2", + "name": "prod-foo", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "shivansh-rai", + "display_id": "DEVU-1", + "display_name": "shivansh-rai", + "email": "shivansh.rai@devrev.ai", + "full_name": "Shivansh Rai", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/1", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-1", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Shivansh%20Rai.png" + } + ], + "stage": { + "name": "" + } + }, + "body": "\u003cdon:core:dvrv-us-1:devo/11FVC3ScK:issue/98\u003e", + "created_by": { + "type": "dev_user", + "display_handle": "shivansh-rai", + "display_id": "DEVU-1", + "display_name": "shivansh-rai", + "email": "shivansh.rai@devrev.ai", + "full_name": "Shivansh Rai", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/1", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-1", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Shivansh%20Rai.png" + }, + "created_date": "2025-06-14T13:20:46.963Z", + "custom_fields": { + "ctype__color": "Blue", + "tnt__a_rich_text_field": "A new issue \u003cdon:core:dvrv-us-1:devo/11FVC3ScK:issue/91\u003e", + "tnt__a_text_field": "" + }, + "custom_schema_fragments": [ + "don:core:dvrv-us-1:devo/11FVC3ScK:tenant_fragment/503", + "don:core:dvrv-us-1:devo/11FVC3ScK:custom_type_fragment/480" + ], + "display_id": "ISS-98", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:issue/98", + "id_v1": "don:DEV-11FVC3ScK:issue:98", + "modified_by": { + "type": "dev_user", + "display_handle": "shivansh-rai", + "display_id": "DEVU-1", + "display_name": "shivansh-rai", + "email": "shivansh.rai@devrev.ai", + "full_name": "Shivansh Rai", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/1", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-1", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Shivansh%20Rai.png" + }, + "modified_date": "2025-07-02T13:07:40.704Z", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "gowtham-tg", + "display_id": "DEVU-10", + "display_name": "gowtham-tg", + "email": "gowtham.tg@devrev.ai", + "full_name": "Gowtham Gopinath", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/10", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-10", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Gowtham%20Gopinath.png" + } + ], + "priority": "p2", + "priority_v2": { + "id": 3, + "label": "P2", + "ordinal": 3 + }, + "references": [ + { + "type": "issue", + "display_id": "ISS-98", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:issue/98", + "id_v1": "don:DEV-11FVC3ScK:issue:98", + "owned_by": [ + { + "type": "dev_user", + "display_handle": "gowtham-tg", + "display_id": "DEVU-10", + "display_name": "gowtham-tg", + "email": "gowtham.tg@devrev.ai", + "full_name": "Gowtham Gopinath", + "id": "don:identity:dvrv-us-1:devo/11FVC3ScK:devu/10", + "id_v1": "don:DEV-11FVC3ScK:dev_user:DEVU-10", + "state": "active", + "thumbnail": "https://api.dev.devrev-eng.ai/internal/display-picture/Gowtham%20Gopinath.png" + } + ], + "priority": "p2", + "priority_v2": { + "id": 3, + "label": "P2", + "ordinal": 3 + }, + "stage": { + "name": "prioritized", + "stage": { + "display_name": "Prioritized", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:custom_stage/27", + "name": "prioritized" + } + }, + "title": "issue with a subtype" + } + ], + "stage": { + "display_name": "Prioritized", + "name": "prioritized", + "notes": "", + "ordinal": 3600, + "stage": { + "display_name": "Prioritized", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:custom_stage/27", + "name": "prioritized" + }, + "state": { + "display_name": "Open", + "id": "don:core:dvrv-us-1:devo/11FVC3ScK:custom_state/1", + "is_final": false, + "name": "open" + } + }, + "state": "open", + "stock_schema_fragment": "don:core:dvrv-us-1:stock_sf/5774789", + "subtype": "test", + "title": "issue with a subtype" + } + }, + "type": "work_updated" +} +``` diff --git a/webhooks/go.mod b/webhooks/go.mod new file mode 100644 index 0000000..6b9d109 --- /dev/null +++ b/webhooks/go.mod @@ -0,0 +1,5 @@ +module devrev-webhooks + +go 1.24.1 + +require github.com/joho/godotenv v1.5.1 diff --git a/webhooks/go.sum b/webhooks/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/webhooks/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/webhooks/main.go b/webhooks/main.go new file mode 100644 index 0000000..411997c --- /dev/null +++ b/webhooks/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "log" + "net/http" + "os" + "time" +) + +type WebhookPayload struct { + ID string `json:"id"` + WebhookID string `json:"webhook_id"` + Timestamp time.Time `json:"timestamp"` + Type string `json:"type"` + Challenge string `json:"challenge"` +} + +func main() { + port := "3000" + if envPort := os.Getenv("PORT"); envPort != "" { + port = envPort + } + http.HandleFunc("/", webhookHandler) + log.Printf("Server starting on port %s\n", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func webhookHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, body, "", " "); err != nil { + log.Printf("Failed to format JSON: %v\n", err) + log.Printf("Raw payload: %s\n", string(body)) + } else { + log.Printf("Received webhook payload:\n%s\n", prettyJSON.String()) + } + + var payload WebhookPayload + if err := json.Unmarshal(body, &payload); err != nil { + http.Error(w, "Failed to parse JSON payload", http.StatusBadRequest) + return + } + if payload.Type == "verify" { + log.Println("Received verification challenge") + response := map[string]string{"challenge": payload.Challenge} + json.NewEncoder(w).Encode(response) + return + } + + signature := r.Header.Get("X-DevRev-Signature") + if !verifySignature(body, signature) { + http.Error(w, "Invalid signature", http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func verifySignature(payload []byte, signature string) bool { + secret := os.Getenv("WEBHOOK_SECRET") + if secret == "" { + panic("WEBHOOK_SECRET not set") + } + h := hmac.New(sha256.New, []byte(secret)) + h.Write(payload) + calculatedSignature := hex.EncodeToString(h.Sum(nil)) + return hmac.Equal([]byte(signature), []byte(calculatedSignature)) +} From 1c6819497cfc2e10712e647c0592c1b916aba0ee Mon Sep 17 00:00:00 2001 From: Shivansh Rai Date: Wed, 2 Jul 2025 19:26:28 +0530 Subject: [PATCH 2/2] Make `global_checks` happy --- .devrev/repo.yml | 1 + .github/CODEOWNERS | 1 + 2 files changed, 2 insertions(+) create mode 100644 .devrev/repo.yml create mode 100644 .github/CODEOWNERS diff --git a/.devrev/repo.yml b/.devrev/repo.yml new file mode 100644 index 0000000..3dadff4 --- /dev/null +++ b/.devrev/repo.yml @@ -0,0 +1 @@ +deployable: false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ffd5dac --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +@shivansh