Monorepo for Tangled tangled.org

docs: document webhooks #1074

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

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3meq4k5prmo22
+162 -162
Interdiff #0 #1
+162 -162
docs/DOCS.md
··· 278 278 git push tangled main 279 279 ``` 280 280 281 - # Webhooks 282 - 283 - Webhooks allow you to receive HTTP POST notifications when events occur in your repositories. This enables you to integrate Tangled with external services, trigger CI/CD pipelines, send notifications, or automate workflows. 284 - 285 - ## Overview 286 - 287 - Webhooks send HTTP POST requests to URLs you configure whenever specific events happen. Currently, Tangled supports push events, with more event types coming soon. 288 - 289 - ## Configuring webhooks 290 - 291 - To set up a webhook for your repository: 292 - 293 - 1. Navigate to your repository settings 294 - 2. Click the "hooks" tab 295 - 3. Click "add webhook" 296 - 4. Configure your webhook: 297 - - **Payload URL**: The endpoint that will receive the webhook POST requests 298 - - **Secret**: An optional secret key for verifying webhook authenticity (auto-generated if left blank) 299 - - **Events**: Select which events trigger the webhook (currently only push events) 300 - - **Active**: Toggle whether the webhook is enabled 301 - 302 - ## Webhook payload 303 - 304 - ### Push 305 - 306 - When a push event occurs, Tangled sends a POST request with a JSON payload of the format: 307 - 308 - ```json 309 - { 310 - "after": "7b320e5cbee2734071e4310c1d9ae401d8f6cab5", 311 - "before": "c04ddf64eddc90e4e2a9846ba3b43e67a0e2865e", 312 - "pusher": { 313 - "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 314 - }, 315 - "ref": "refs/heads/main", 316 - "repository": { 317 - "clone_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 318 - "created_at": "2025-09-15T08:57:23Z", 319 - "description": "an example repository", 320 - "fork": false, 321 - "full_name": "did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 322 - "html_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 323 - "name": "some-repo", 324 - "open_issues_count": 5, 325 - "owner": { 326 - "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 327 - }, 328 - "ssh_url": "ssh://git@tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 329 - "stars_count": 1, 330 - "updated_at": "2025-09-15T08:57:23Z" 331 - } 332 - } 333 - ``` 334 - 335 - ## HTTP headers 336 - 337 - Each webhook request includes the following headers: 338 - 339 - - `Content-Type: application/json` 340 - - `User-Agent: Tangled-Hook/<short-sha>` — User agent with short SHA of the commit 341 - - `X-Tangled-Event: push` — The event type 342 - - `X-Tangled-Hook-ID: <webhook-id>` — The webhook ID 343 - - `X-Tangled-Delivery: <uuid>` — Unique delivery ID 344 - - `X-Tangled-Signature-256: sha256=<hmac>` — HMAC-SHA256 signature (if secret configured) 345 - 346 - ## Verifying webhook signatures 347 - 348 - If you configured a secret, you should verify the webhook signature to ensure requests are authentic. For example, in Go: 349 - 350 - ```go 351 - package main 352 - 353 - import ( 354 - "crypto/hmac" 355 - "crypto/sha256" 356 - "encoding/hex" 357 - "io" 358 - "net/http" 359 - "strings" 360 - ) 361 - 362 - func verifySignature(payload []byte, signatureHeader, secret string) bool { 363 - // Remove 'sha256=' prefix from signature header 364 - signature := strings.TrimPrefix(signatureHeader, "sha256=") 365 - 366 - // Compute expected signature 367 - mac := hmac.New(sha256.New, []byte(secret)) 368 - mac.Write(payload) 369 - expected := hex.EncodeToString(mac.Sum(nil)) 370 - 371 - // Use constant-time comparison to prevent timing attacks 372 - return hmac.Equal([]byte(signature), []byte(expected)) 373 - } 374 - 375 - func webhookHandler(w http.ResponseWriter, r *http.Request) { 376 - // Read the request body 377 - payload, err := io.ReadAll(r.Body) 378 - if err != nil { 379 - http.Error(w, "Bad request", http.StatusBadRequest) 380 - return 381 - } 382 - 383 - // Get signature from header 384 - signatureHeader := r.Header.Get("X-Tangled-Signature-256") 385 - 386 - // Verify signature 387 - if signatureHeader != "" && verifySignature(payload, signatureHeader, yourSecret) { 388 - // Webhook is authentic, process it 389 - processWebhook(payload) 390 - w.WriteHeader(http.StatusOK) 391 - } else { 392 - http.Error(w, "Invalid signature", http.StatusUnauthorized) 393 - } 394 - } 395 - ``` 396 - 397 - ## Delivery retries 398 - 399 - Webhooks are automatically retried on failure: 400 - 401 - - **3 total attempts** (1 initial + 2 retries) 402 - - **Exponential backoff** starting at 1 second, max 10 seconds 403 - - **Retried on**: 404 - - Network errors 405 - - HTTP 5xx server errors 406 - - **Not retried on**: 407 - - HTTP 4xx client errors (bad request, unauthorized, etc.) 408 - 409 - ### Timeouts 410 - 411 - Webhook requests timeout after 30 seconds. If your endpoint needs more time: 412 - 413 - 1. Respond with 200 OK immediately 414 - 2. Process the webhook asynchronously in the background 415 - 416 - ## Example integrations 417 - 418 - ### Discord notifications 419 - 420 - ```javascript 421 - app.post("/webhook", (req, res) => { 422 - const payload = req.body; 423 - 424 - fetch("https://discord.com/api/webhooks/...", { 425 - method: "POST", 426 - headers: { "Content-Type": "application/json" }, 427 - body: JSON.stringify({ 428 - content: `New push to ${payload.repository.full_name}`, 429 - embeds: [ 430 - { 431 - title: `${payload.pusher.login} pushed to ${payload.ref}`, 432 - url: payload.repository.html_url, 433 - color: 0x00ff00, 434 - }, 435 - ], 436 - }), 437 - }); 438 - 439 - res.status(200).send("OK"); 440 - }); 441 - ``` 442 - 443 281 # Knot self-hosting guide 444 282 445 283 So you want to run your own knot server? Great! Here are a few prerequisites: ··· 1383 1221 secret_id="$(cat /tmp/openbao/secret-id)" 1384 1222 ``` 1385 1223 1224 + # Webhooks 1225 + 1226 + Webhooks allow you to receive HTTP POST notifications when events occur in your repositories. This enables you to integrate Tangled with external services, trigger CI/CD pipelines, send notifications, or automate workflows. 1227 + 1228 + ## Overview 1229 + 1230 + Webhooks send HTTP POST requests to URLs you configure whenever specific events happen. Currently, Tangled supports push events, with more event types coming soon. 1231 + 1232 + ## Configuring webhooks 1233 + 1234 + To set up a webhook for your repository: 1235 + 1236 + 1. Navigate to your repository settings 1237 + 2. Click the "hooks" tab 1238 + 3. Click "add webhook" 1239 + 4. Configure your webhook: 1240 + - **Payload URL**: The endpoint that will receive the webhook POST requests 1241 + - **Secret**: An optional secret key for verifying webhook authenticity (auto-generated if left blank) 1242 + - **Events**: Select which events trigger the webhook (currently only push events) 1243 + - **Active**: Toggle whether the webhook is enabled 1244 + 1245 + ## Webhook payload 1246 + 1247 + ### Push 1248 + 1249 + When a push event occurs, Tangled sends a POST request with a JSON payload of the format: 1250 + 1251 + ```json 1252 + { 1253 + "after": "7b320e5cbee2734071e4310c1d9ae401d8f6cab5", 1254 + "before": "c04ddf64eddc90e4e2a9846ba3b43e67a0e2865e", 1255 + "pusher": { 1256 + "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 1257 + }, 1258 + "ref": "refs/heads/main", 1259 + "repository": { 1260 + "clone_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1261 + "created_at": "2025-09-15T08:57:23Z", 1262 + "description": "an example repository", 1263 + "fork": false, 1264 + "full_name": "did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1265 + "html_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1266 + "name": "some-repo", 1267 + "open_issues_count": 5, 1268 + "owner": { 1269 + "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 1270 + }, 1271 + "ssh_url": "ssh://git@tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1272 + "stars_count": 1, 1273 + "updated_at": "2025-09-15T08:57:23Z" 1274 + } 1275 + } 1276 + ``` 1277 + 1278 + ## HTTP headers 1279 + 1280 + Each webhook request includes the following headers: 1281 + 1282 + - `Content-Type: application/json` 1283 + - `User-Agent: Tangled-Hook/<short-sha>` — User agent with short SHA of the commit 1284 + - `X-Tangled-Event: push` — The event type 1285 + - `X-Tangled-Hook-ID: <webhook-id>` — The webhook ID 1286 + - `X-Tangled-Delivery: <uuid>` — Unique delivery ID 1287 + - `X-Tangled-Signature-256: sha256=<hmac>` — HMAC-SHA256 signature (if secret configured) 1288 + 1289 + ## Verifying webhook signatures 1290 + 1291 + If you configured a secret, you should verify the webhook signature to ensure requests are authentic. For example, in Go: 1292 + 1293 + ```go 1294 + package main 1295 + 1296 + import ( 1297 + "crypto/hmac" 1298 + "crypto/sha256" 1299 + "encoding/hex" 1300 + "io" 1301 + "net/http" 1302 + "strings" 1303 + ) 1304 + 1305 + func verifySignature(payload []byte, signatureHeader, secret string) bool { 1306 + // Remove 'sha256=' prefix from signature header 1307 + signature := strings.TrimPrefix(signatureHeader, "sha256=") 1308 + 1309 + // Compute expected signature 1310 + mac := hmac.New(sha256.New, []byte(secret)) 1311 + mac.Write(payload) 1312 + expected := hex.EncodeToString(mac.Sum(nil)) 1313 + 1314 + // Use constant-time comparison to prevent timing attacks 1315 + return hmac.Equal([]byte(signature), []byte(expected)) 1316 + } 1317 + 1318 + func webhookHandler(w http.ResponseWriter, r *http.Request) { 1319 + // Read the request body 1320 + payload, err := io.ReadAll(r.Body) 1321 + if err != nil { 1322 + http.Error(w, "Bad request", http.StatusBadRequest) 1323 + return 1324 + } 1325 + 1326 + // Get signature from header 1327 + signatureHeader := r.Header.Get("X-Tangled-Signature-256") 1328 + 1329 + // Verify signature 1330 + if signatureHeader != "" && verifySignature(payload, signatureHeader, yourSecret) { 1331 + // Webhook is authentic, process it 1332 + processWebhook(payload) 1333 + w.WriteHeader(http.StatusOK) 1334 + } else { 1335 + http.Error(w, "Invalid signature", http.StatusUnauthorized) 1336 + } 1337 + } 1338 + ``` 1339 + 1340 + ## Delivery retries 1341 + 1342 + Webhooks are automatically retried on failure: 1343 + 1344 + - **3 total attempts** (1 initial + 2 retries) 1345 + - **Exponential backoff** starting at 1 second, max 10 seconds 1346 + - **Retried on**: 1347 + - Network errors 1348 + - HTTP 5xx server errors 1349 + - **Not retried on**: 1350 + - HTTP 4xx client errors (bad request, unauthorized, etc.) 1351 + 1352 + ### Timeouts 1353 + 1354 + Webhook requests timeout after 30 seconds. If your endpoint needs more time: 1355 + 1356 + 1. Respond with 200 OK immediately 1357 + 2. Process the webhook asynchronously in the background 1358 + 1359 + ## Example integrations 1360 + 1361 + ### Discord notifications 1362 + 1363 + ```javascript 1364 + app.post("/webhook", (req, res) => { 1365 + const payload = req.body; 1366 + 1367 + fetch("https://discord.com/api/webhooks/...", { 1368 + method: "POST", 1369 + headers: { "Content-Type": "application/json" }, 1370 + body: JSON.stringify({ 1371 + content: `New push to ${payload.repository.full_name}`, 1372 + embeds: [ 1373 + { 1374 + title: `${payload.pusher.did} pushed to ${payload.ref}`, 1375 + url: payload.repository.html_url, 1376 + color: 0x00ff00, 1377 + }, 1378 + ], 1379 + }), 1380 + }); 1381 + 1382 + res.status(200).send("OK"); 1383 + }); 1384 + ``` 1385 + 1386 1386 # Migrating knots and spindles 1387 1387 1388 1388 Sometimes, non-backwards compatible changes are made to the

History

5 rounds 1 comment
sign up or login to add to the discussion
1 commit
expand
docs: document webhooks
3/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
docs: document webhooks
3/3 success
expand
expand 0 comments
1 commit
expand
docs: document webhooks
3/3 success
expand
expand 0 comments
1 commit
expand
docs: document webhooks
3/3 success
expand
expand 0 comments
1 commit
expand
docs: document webhooks
3/3 success
expand
expand 1 comment
  • here, the payload does not include pusher.login, only pusher.did
  • i think the webhooks section can go after the knots and spindles sections

changeset lgtm otherwise!

will give this feature a test locally to identify bugs if any!