markdown · 3970 bytes Raw Blame History

Webhooks

Webhooks send HTTP POSTs to your URL when something happens in a repo (push, PR opened, issue commented, etc.). Configured at Repository → Settings → Webhooks → "Add webhook".

Configuration

  • Payload URL — the HTTPS endpoint we POST to. HTTP is rejected on production instances.
  • Content typeapplication/json (default) or application/x-www-form-urlencoded.
  • Secret — used to HMAC-sign each delivery. We strongly recommend setting one. The secret is stored AEAD-encrypted at rest; you cannot retrieve it after creation, only replace it.
  • Events — pick "Just push", "Send everything", or specific events.
  • Active — toggle without deleting.

Signature verification

Each delivery includes:

  • X-Shithub-Event: <event-name> — e.g., push, pull_request, workflow_run.
  • X-Shithub-Delivery: <uuid> — unique per delivery (idempotent).
  • X-Shithub-Signature-256: sha256=<hex> — HMAC-SHA256 of the raw body using your configured secret.

Always verify the signature before trusting the payload. Constant-time comparison; never compare with ==.

Go

func verify(body []byte, sig, secret string) bool {
    sig = strings.TrimPrefix(sig, "sha256=")
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(expected))
}

Python

import hmac, hashlib

def verify(body: bytes, sig: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    sig = sig.removeprefix("sha256=")
    return hmac.compare_digest(sig, expected)

Node.js

const crypto = require("crypto");

function verify(body, sig, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected),
  );
}

The body must be the raw request body, not the parsed JSON. Frameworks that auto-parse will give you the wrong bytes.

Idempotency

Use X-Shithub-Delivery as your idempotency key. We may retry a delivery if your endpoint returns 5xx or times out, so processing the same delivery twice should be safe in your system.

Retries

A delivery is retried on:

  • Network error.
  • 5xx response.
  • Timeout (default 10s).

Retry schedule: exponential backoff with jitter, up to ~6 retries over ~24h. After 50 consecutive failures, the webhook auto- disables to stop bombarding a broken endpoint. You'll see a banner on the webhook config page; flip "Active" back on once the endpoint is fixed.

Inspecting deliveries

Webhook detail page → "Recent deliveries". Each row shows:

  • Event + delivery ID + timestamp.
  • Request headers + (truncated) body we sent.
  • Response status + headers + (truncated) body we got back.
  • "Redeliver" — re-sends the original payload with the same signature.

Stored bodies are capped at 32 KiB (your endpoint can accept bigger; we just don't keep more for the inspector).

Actions events

Repository webhooks can subscribe to Actions lifecycle events:

  • workflow_run actions: queued, running, completed.
  • workflow_job actions: queued, running, completed, cancelled.

Actions payloads only carry structural run/job metadata. shithub does not include workflow event payloads, env, permissions, logs, runner tokens, or secrets in webhook bodies.

SSRF defense

shithub validates webhook URLs server-side: hostnames are resolved, IPs are checked against a block-list (RFC1918, link- local, loopback, multicast, 169.254.0.0/16, etc.), and the request is dialed to the resolved IP — no following CNAMEs into internal address space at delivery time.

Operators of self-hosted instances can opt in to private destinations via an AllowedHosts list — see self-host configuration.

View source
1 # Webhooks
2
3 Webhooks send HTTP POSTs to your URL when something happens in a
4 repo (push, PR opened, issue commented, etc.). Configured at
5 Repository → Settings → Webhooks → "Add webhook".
6
7 ## Configuration
8
9 - **Payload URL** — the HTTPS endpoint we POST to. HTTP is
10 rejected on production instances.
11 - **Content type** — `application/json` (default) or
12 `application/x-www-form-urlencoded`.
13 - **Secret** — used to HMAC-sign each delivery. We strongly
14 recommend setting one. The secret is stored AEAD-encrypted at
15 rest; you cannot retrieve it after creation, only replace it.
16 - **Events** — pick "Just push", "Send everything", or specific
17 events.
18 - **Active** — toggle without deleting.
19
20 ## Signature verification
21
22 Each delivery includes:
23
24 - `X-Shithub-Event: <event-name>` — e.g., `push`, `pull_request`,
25 `workflow_run`.
26 - `X-Shithub-Delivery: <uuid>` — unique per delivery (idempotent).
27 - `X-Shithub-Signature-256: sha256=<hex>` — HMAC-SHA256 of the
28 raw body using your configured secret.
29
30 **Always verify the signature before trusting the payload.**
31 Constant-time comparison; never compare with `==`.
32
33 ### Go
34
35 ```go
36 func verify(body []byte, sig, secret string) bool {
37 sig = strings.TrimPrefix(sig, "sha256=")
38 mac := hmac.New(sha256.New, []byte(secret))
39 mac.Write(body)
40 expected := hex.EncodeToString(mac.Sum(nil))
41 return hmac.Equal([]byte(sig), []byte(expected))
42 }
43 ```
44
45 ### Python
46
47 ```python
48 import hmac, hashlib
49
50 def verify(body: bytes, sig: str, secret: str) -> bool:
51 expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
52 sig = sig.removeprefix("sha256=")
53 return hmac.compare_digest(sig, expected)
54 ```
55
56 ### Node.js
57
58 ```js
59 const crypto = require("crypto");
60
61 function verify(body, sig, secret) {
62 const expected = "sha256=" + crypto
63 .createHmac("sha256", secret)
64 .update(body)
65 .digest("hex");
66 return crypto.timingSafeEqual(
67 Buffer.from(sig),
68 Buffer.from(expected),
69 );
70 }
71 ```
72
73 The body must be the **raw request body**, not the parsed JSON.
74 Frameworks that auto-parse will give you the wrong bytes.
75
76 ## Idempotency
77
78 Use `X-Shithub-Delivery` as your idempotency key. We may retry a
79 delivery if your endpoint returns 5xx or times out, so processing
80 the same delivery twice should be safe in your system.
81
82 ## Retries
83
84 A delivery is retried on:
85
86 - Network error.
87 - 5xx response.
88 - Timeout (default 10s).
89
90 Retry schedule: exponential backoff with jitter, up to ~6 retries
91 over ~24h. After 50 consecutive failures, the webhook **auto-
92 disables** to stop bombarding a broken endpoint. You'll see a
93 banner on the webhook config page; flip "Active" back on once
94 the endpoint is fixed.
95
96 ## Inspecting deliveries
97
98 Webhook detail page → "Recent deliveries". Each row shows:
99
100 - Event + delivery ID + timestamp.
101 - Request headers + (truncated) body we sent.
102 - Response status + headers + (truncated) body we got back.
103 - "Redeliver" — re-sends the original payload with the same
104 signature.
105
106 Stored bodies are capped at 32 KiB (your endpoint can accept
107 bigger; we just don't keep more for the inspector).
108
109 ## Actions events
110
111 Repository webhooks can subscribe to Actions lifecycle events:
112
113 - `workflow_run` actions: `queued`, `running`, `completed`.
114 - `workflow_job` actions: `queued`, `running`, `completed`,
115 `cancelled`.
116
117 Actions payloads only carry structural run/job metadata. shithub does
118 not include workflow event payloads, env, permissions, logs, runner
119 tokens, or secrets in webhook bodies.
120
121 ## SSRF defense
122
123 shithub validates webhook URLs server-side: hostnames are
124 resolved, IPs are checked against a block-list (RFC1918, link-
125 local, loopback, multicast, 169.254.0.0/16, etc.), and the
126 request is dialed to the resolved IP — no following CNAMEs into
127 internal address space at delivery time.
128
129 Operators of self-hosted instances can opt in to private
130 destinations via an `AllowedHosts` list — see
131 [self-host configuration](../self-host/configuration.md).