forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
6.0 KiB
1# Webhooks (contract between workers)
2
3The global rules from the earlier interface docs apply:
4- No JS and no inline styles; no unwrap in non-test code.
5- Tests use `tempfile::TempDir`.
6- Never put cargo target dirs or big files in /tmp.
7- Every new crate is added at its **latest** version with `cargo add` and recorded in the root `[workspace.dependencies]`.
8
9## Model
10- A repo webhook has:
11 - a URL (https, or http only when the config allows it);
12 - a secret, encrypted at rest with the instance key the same way TOTP secrets are;
13 - a content type (always `application/json`);
14 - an event list: `push`, `pull_request`, `issues`, `issue_comment`, `pull_request_review`, `create`, `delete`, `ping`;
15 - an `active` flag.
16- Managing hooks needs Admin on the repo.
17- **Deliveries** are rows in the DB:
18 - the request: headers and body. The headers are queued without `X-Forge-Delivery` or the signature; after each attempt the stored headers are replaced with the ones actually sent;
19 - the response: status, the first 64 KiB of the body, and timing;
20 - `attempt`, a state (`pending` | `ok` | `failed` | `gave_up`), `next_attempt_at`, and a GUID.
21- **Retries** use backoff 1m, 5m, 30m, 2h, 6h, then `gave_up`. "Redeliver" in the UI creates a new delivery with the same payload.
22- **Headers:**
23 - `X-Forge-Event: <event>`, `X-Forge-Delivery: <guid>`;
24 - `X-Forge-Signature-256: sha256=<hex HMAC-SHA256(secret, raw body)>`;
25 - `User-Agent: forge-hookshot/<version>`.
26- **Payloads** are our own JSON. We deliberately don't copy the GitHub or Gitea schemas, but a similar shape is fine:
27 - `repository {id, owner, name, full_name, private, html_url, clone_url}`, `sender {id, username}`;
28 - push: `ref, before, after, commits[{id, message, author{name,email}, timestamp, url}] (max 20), total_commits, compare_url`;
29 - pull_request: `action (opened|closed|reopened|merged|synchronize|edited|labeled|unlabeled|assigned|unassigned|milestoned|demilestoned), number, pull_request {title, body, state, merged, base, head, html_url}`; the label, assignee and milestone actions add `label {name, color}`, `assignee {username}` or `milestone {title}` at the top level, as the issues event does;
30 - issues: `action (opened|closed|reopened|edited|labeled|unlabeled|assigned|unassigned|milestoned|demilestoned), issue {number, title, body, state, html_url}`, plus `label {name, color}`, `assignee {username}` or `milestone {title}` for those actions;
31 - issue_comment: `action (created|edited|deleted), issue {…}, comment {id, body, html_url}`;
32 - pull_request_review: `action submitted, review {id, state, body}, pull_request {…}`.
33
34## SSRF guard (mandatory)
35- Resolve the host once, check every resolved address, then connect to exactly that checked address.
36 - In reqwest, use a custom `dns::Resolve` that filters addresses, so a DNS rebind can't bypass the check.
37- Reject: loopback, private (RFC1918, fc00::/7), link-local (169.254/16, fe80::/10), CGNAT 100.64/10, unspecified, multicast, broadcast, documentation ranges, IPv4-mapped IPv6 of any of these, and 0.0.0.0/8.
38- Config `[webhooks] allowed_hosts = []` (exact hostnames or CIDRs) whitelists internal targets; the default is empty.
39- **No redirects.** Timeouts: 10s connect, 20s total. Response bodies are read up to 64 KiB.
40- The URL must have no userinfo, and its port must be 1–65535.
41- `allow_http = false` by default.
42
43## Delivery worker
44- A tokio task started at boot. Each loop it claims due deliveries (`state = 'pending' AND next_attempt_at <= now`), sends them with bounded concurrency (4), and records the result.
45- A 2xx response means `ok`.
46- It is woken by a `Notify` when a new delivery is queued, so the first attempt is immediate; otherwise it polls every 30s.
47- Deliveries older than 14 days are purged daily by the same task.
48
49## Integration points
50- Enqueue through the sender-aware helpers in `crates/forge-web/src/webhooks/emit.rs`. Each one builds its event's payload, looks up the sender, and hands off to the generic `emit(state, repo, sender, event, action, payload)`. That function finds the active hooks subscribed to the event and inserts a delivery for each. Nothing here fails the caller; errors are logged.
51 - `Sender` says who made the change. `lookup_sender(state, actor_id)` builds one; an empty id means a system change and is sent as `"sender": null`.
52 - `emit_ref_update(…)`: `push`, plus `create`/`delete` for branch and tag creation and deletion. Only `refs/heads/*` and `refs/tags/*` produce events.
53 - `emit_pull(…)`: `pull_request` (for example, on PR create in `pages/compare.rs`).
54 - `emit_item_action(…)`: `pull_request` or `issues` action events, reloaded from the store. Used for close, reopen, merge and `synchronize`.
55 - `emit_issue(…)`: `issues` create (`pages/issues_list.rs`).
56 - `emit_label_changes(…)` and `emit_assignee_changes(…)`: one `labeled`/`unlabeled` or `assigned`/`unassigned` event per changed label (matched by name) or username. `pull` picks the `pull_request` event for a pull request and `issues` for an issue.
57 - `emit_milestone_change(…)`: `demilestoned` for the milestone before and `milestoned` for the one after, given their titles; nothing when they match. Same `pull` choice.
58 - `emit_comment(…)`: `issue_comment` create, edit and delete.
59 - `emit_review(…)`: `pull_request_review` submit.
60- Call sites: `post_update.rs`, `pulls_sync.rs`, `pages/{compare,issue_view,issue_parts,issue_meta,issues_list,pulls_merge,pull_reviews}.rs`. The label, assignee and milestone saves in `pages/issue_meta.rs` run for issues and pull requests alike.
61- A "ping" event is sent by `emit_ping(state, hook)` when a hook is created and from the "Test" button. It ignores the hook's subscriptions.
62
63## UI (repo settings, a "Webhooks" section)
64- List hooks with their last delivery status.
65- Create and edit form: URL, secret (write-only; leave it blank to keep the current one), event checkboxes, active.
66- Delete hooks.
67- Hook detail page: the last 50 deliveries, each expandable with `<details>` to show the request headers and body and the response; a Redeliver button and a Test (ping) button.